@indigoai-us/hq-cloud 6.15.63 → 6.15.65

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_FULL_RECONCILE_MS, EVENT_BATCH_LIMIT_ENV, FULL_RECONCILE_MS_ENV, 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_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";
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";
@@ -4340,6 +4340,63 @@ describe("runRunnerWithLoop — event-push wiring", () => {
4340
4340
  triggerShutdown();
4341
4341
  await loop;
4342
4342
  });
4343
+ it("pushes a file created before watcher ready through the warm-up catch-up", async () => {
4344
+ const watcher = makeBatchWatcherStub();
4345
+ let markReady = () => { };
4346
+ watcher.onReady = (listener) => {
4347
+ markReady = listener;
4348
+ return () => { };
4349
+ };
4350
+ watcher.collectWarmupCatchup = vi.fn(() => ({
4351
+ batch: {
4352
+ paths: new Map([[
4353
+ "/tmp/hq/companies/indigo/knowledge/created-during-warmup.md",
4354
+ "companies/indigo/knowledge/created-during-warmup.md",
4355
+ ]]),
4356
+ changes: new Map([[
4357
+ "/tmp/hq/companies/indigo/knowledge/created-during-warmup.md",
4358
+ { kind: "add" },
4359
+ ]]),
4360
+ },
4361
+ scannedPaths: 1,
4362
+ }));
4363
+ const sleep = makeSteppableSleep();
4364
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4365
+ let shutdown = () => { };
4366
+ const loop = runRunnerWithLoop([
4367
+ "--company", "indigo", "--watch", "--event-push", "--direction", "pull",
4368
+ "--hq-root", "/tmp/hq", "--poll-remote-ms", "60000",
4369
+ ], {
4370
+ runPass,
4371
+ createWatcher: () => watcher,
4372
+ sleep: sleep.sleep,
4373
+ onShutdownSignal: (handler) => {
4374
+ shutdown = handler;
4375
+ return () => { };
4376
+ },
4377
+ });
4378
+ try {
4379
+ await flushLoopMicrotasks();
4380
+ markReady();
4381
+ await flushLoopMicrotasks();
4382
+ expect(watcher.collectWarmupCatchup).toHaveBeenCalledOnce();
4383
+ expect(runPass.mock.calls.map(([argv]) => argv)).toEqual([
4384
+ ["--company", "indigo", "--direction", "pull", "--hq-root", "/tmp/hq"],
4385
+ [
4386
+ "--company", "indigo", "--direction", "push",
4387
+ "--scope-path", "knowledge/created-during-warmup.md", "--hq-root", "/tmp/hq",
4388
+ ],
4389
+ [
4390
+ "--company", "indigo", "--direction", "pull",
4391
+ "--scope-path", "knowledge/created-during-warmup.md", "--hq-root", "/tmp/hq",
4392
+ ],
4393
+ ]);
4394
+ }
4395
+ finally {
4396
+ shutdown();
4397
+ await loop;
4398
+ }
4399
+ });
4343
4400
  it("keeps every event surface off until a result proves maintenance completed", async () => {
4344
4401
  const watcher = makeWatcherStub();
4345
4402
  const sleep = makeSteppableSleep();
@@ -4506,10 +4563,10 @@ describe("runRunnerWithLoop — event-push wiring", () => {
4506
4563
  await flushLoopMicrotasks();
4507
4564
  expect(sleepCalls).toEqual([600_000]);
4508
4565
  // The fake 600-second timer remains unresolved. The watcher wake must
4509
- // nonetheless start the next pass immediately through runGuarded.
4566
+ // nonetheless start its bounded push and configured pull immediately.
4510
4567
  watcher.emit("companies/indigo/board.json");
4511
4568
  await flushLoopMicrotasks();
4512
- expect(runPass).toHaveBeenCalledTimes(2);
4569
+ expect(runPass).toHaveBeenCalledTimes(3);
4513
4570
  triggerShutdown();
4514
4571
  await loop;
4515
4572
  });
@@ -4592,6 +4649,528 @@ describe("runRunnerWithLoop — event-push wiring", () => {
4592
4649
  triggerShutdown();
4593
4650
  await loop;
4594
4651
  });
4652
+ it("fast lane: a small watcher batch dispatches during a slow bulk pass", async () => {
4653
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-watch-"));
4654
+ const relativePath = "companies/indigo/realtime.md";
4655
+ const absolutePath = path.join(hqRoot, relativePath);
4656
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
4657
+ fs.writeFileSync(absolutePath, "tiny realtime edit");
4658
+ const watcher = makeBatchWatcherStub();
4659
+ let triggerShutdown = () => { };
4660
+ let releaseBulk = () => { };
4661
+ const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
4662
+ const order = [];
4663
+ let fullPasses = 0;
4664
+ const runPass = vi.fn().mockImplementation(async (passArgv) => {
4665
+ if (passArgv.includes("--scope-path")) {
4666
+ order.push("fast");
4667
+ return completedPassOutcome();
4668
+ }
4669
+ fullPasses += 1;
4670
+ if (fullPasses === 2) {
4671
+ order.push("bulk");
4672
+ await bulkGate;
4673
+ }
4674
+ return completedPassOutcome();
4675
+ });
4676
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "both", "--hq-root", hqRoot], {
4677
+ runPass,
4678
+ clock: new FakeClock(),
4679
+ createWatcher: () => watcher,
4680
+ sleep: () => new Promise(() => { }),
4681
+ onShutdownSignal: (handler) => {
4682
+ triggerShutdown = handler;
4683
+ return () => { };
4684
+ },
4685
+ });
4686
+ await flushLoopMicrotasks();
4687
+ watcher.emit(); // starts the second, deliberately blocked full pass
4688
+ await flushLoopMicrotasks();
4689
+ expect(order).toEqual(["bulk"]);
4690
+ watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
4691
+ await flushLoopMicrotasks(30);
4692
+ // Regression: origin/main leaves this as ["bulk"] until releaseBulk().
4693
+ expect(order).toEqual(["bulk", "fast", "fast"]);
4694
+ releaseBulk();
4695
+ await flushLoopMicrotasks();
4696
+ triggerShutdown();
4697
+ await loop;
4698
+ fs.rmSync(hqRoot, { recursive: true, force: true });
4699
+ });
4700
+ it("fast lane: a small receiver pull dispatches during a slow bulk pass", async () => {
4701
+ const watcher = makeWatcherStub();
4702
+ let triggerShutdown = () => { };
4703
+ let receiverSync;
4704
+ let releaseBulk = () => { };
4705
+ const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
4706
+ const order = [];
4707
+ let fullPasses = 0;
4708
+ const runPass = vi.fn().mockImplementation(async (passArgv) => {
4709
+ if (passArgv.includes("--scope-path")) {
4710
+ order.push("fast-pull");
4711
+ return completedPassOutcome();
4712
+ }
4713
+ fullPasses += 1;
4714
+ if (fullPasses === 2) {
4715
+ order.push("bulk");
4716
+ await bulkGate;
4717
+ }
4718
+ return completedPassOutcome();
4719
+ });
4720
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq-fast-lane-receiver"], {
4721
+ runPass,
4722
+ createWatcher: () => watcher,
4723
+ createReceiver: ({ syncBatchFn }) => {
4724
+ receiverSync = syncBatchFn;
4725
+ return { connected: true, start: async () => { }, dispose: async () => { } };
4726
+ },
4727
+ sleep: () => new Promise(() => { }),
4728
+ onShutdownSignal: (handler) => {
4729
+ triggerShutdown = handler;
4730
+ return () => { };
4731
+ },
4732
+ });
4733
+ await flushLoopMicrotasks();
4734
+ watcher.emit();
4735
+ await flushLoopMicrotasks();
4736
+ expect(order).toEqual(["bulk"]);
4737
+ await receiverSync({
4738
+ events: [{
4739
+ kind: "upsert", relativePath: "companies/acme/realtime.md", contentHash: "sha256:x",
4740
+ mtime: "2026-08-26T00:00:00.000Z", originDeviceId: "peer", originTenantId: "acme",
4741
+ sequenceNumber: 1, eventTimestamp: "2026-08-26T00:00:00.000Z",
4742
+ }],
4743
+ signal: new AbortController().signal,
4744
+ });
4745
+ expect(order).toEqual(["bulk", "fast-pull"]);
4746
+ releaseBulk();
4747
+ await flushLoopMicrotasks();
4748
+ triggerShutdown();
4749
+ await loop;
4750
+ });
4751
+ it("fast lane: retains the root lock until an outliving fast pass completes", async () => {
4752
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-lock-"));
4753
+ const relativePath = "companies/indigo/realtime.md";
4754
+ const absolutePath = path.join(hqRoot, relativePath);
4755
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
4756
+ fs.writeFileSync(absolutePath, "tiny realtime edit");
4757
+ const watcher = makeBatchWatcherStub();
4758
+ let triggerShutdown = () => { };
4759
+ let releaseBulk = () => { };
4760
+ let releaseFast = () => { };
4761
+ let markFastStarted = () => { };
4762
+ const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
4763
+ const fastGate = new Promise((resolve) => { releaseFast = resolve; });
4764
+ const fastStarted = new Promise((resolve) => { markFastStarted = resolve; });
4765
+ let fullPasses = 0;
4766
+ const runPass = vi.fn().mockImplementation(async (passArgv) => {
4767
+ if (passArgv.includes("--scope-path")) {
4768
+ markFastStarted();
4769
+ await fastGate;
4770
+ return completedPassOutcome();
4771
+ }
4772
+ fullPasses += 1;
4773
+ if (fullPasses === 2)
4774
+ await bulkGate;
4775
+ return completedPassOutcome();
4776
+ });
4777
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "both", "--hq-root", hqRoot], {
4778
+ runPass,
4779
+ clock: new FakeClock(),
4780
+ createWatcher: () => watcher,
4781
+ sleep: () => new Promise(() => { }),
4782
+ onShutdownSignal: (handler) => {
4783
+ triggerShutdown = handler;
4784
+ return () => { };
4785
+ },
4786
+ });
4787
+ await flushLoopMicrotasks();
4788
+ watcher.emit();
4789
+ await flushLoopMicrotasks();
4790
+ watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
4791
+ await fastStarted;
4792
+ releaseBulk();
4793
+ await flushLoopMicrotasks(20);
4794
+ // The slow pass has finished, but its fast bypass still mutates. A second
4795
+ // sync/rescue must not acquire the root operation lock in this interval.
4796
+ expect(fs.existsSync(lockPathFor(hqRoot))).toBe(true);
4797
+ releaseFast();
4798
+ await flushLoopMicrotasks(20);
4799
+ expect(fs.existsSync(lockPathFor(hqRoot))).toBe(false);
4800
+ triggerShutdown();
4801
+ await loop;
4802
+ fs.rmSync(hqRoot, { recursive: true, force: true });
4803
+ });
4804
+ it("fast lane: an absent unlinkDir subtree is always slow-lane", () => {
4805
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-unlink-dir-"));
4806
+ const deletedDir = path.join(hqRoot, "companies/indigo/archive");
4807
+ fs.mkdirSync(deletedDir, { recursive: true });
4808
+ fs.rmSync(deletedDir, { recursive: true, force: true });
4809
+ expect(isFastLocalBatch([deletedDir], new Map([[deletedDir, { kind: "unlinkDir" }]]))).toBe(false);
4810
+ fs.rmSync(hqRoot, { recursive: true, force: true });
4811
+ });
4812
+ it("fast lane: --skip-personal rejects a selected-out personal watcher batch", async () => {
4813
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-route-"));
4814
+ const relativePath = "knowledge/private.md";
4815
+ const absolutePath = path.join(hqRoot, relativePath);
4816
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
4817
+ fs.writeFileSync(absolutePath, "private");
4818
+ const watcher = makeBatchWatcherStub();
4819
+ let triggerShutdown = () => { };
4820
+ let releaseBulk = () => { };
4821
+ const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
4822
+ let fullPasses = 0;
4823
+ const runPass = vi.fn().mockImplementation(async (passArgv) => {
4824
+ if (passArgv.includes("--scope-path"))
4825
+ return completedPassOutcome();
4826
+ fullPasses += 1;
4827
+ if (fullPasses === 2)
4828
+ await bulkGate;
4829
+ return completedPassOutcome();
4830
+ });
4831
+ const loop = runRunnerWithLoop(["--companies", "--skip-personal", "--watch", "--event-push", "--direction", "both", "--hq-root", hqRoot], {
4832
+ runPass,
4833
+ clock: new FakeClock(),
4834
+ createWatcher: () => watcher,
4835
+ sleep: () => new Promise(() => { }),
4836
+ onShutdownSignal: (handler) => {
4837
+ triggerShutdown = handler;
4838
+ return () => { };
4839
+ },
4840
+ });
4841
+ await flushLoopMicrotasks();
4842
+ watcher.emit();
4843
+ await flushLoopMicrotasks();
4844
+ runPass.mockClear();
4845
+ watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
4846
+ await flushLoopMicrotasks(20);
4847
+ expect(runPass.mock.calls).toEqual([]);
4848
+ releaseBulk();
4849
+ await flushLoopMicrotasks();
4850
+ triggerShutdown();
4851
+ await loop;
4852
+ fs.rmSync(hqRoot, { recursive: true, force: true });
4853
+ });
4854
+ it("fast lane: signals the V2 scheduler for an immediate watcher dispatch", async () => {
4855
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-signal-"));
4856
+ const relativePath = "companies/indigo/realtime.md";
4857
+ const absolutePath = path.join(hqRoot, relativePath);
4858
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
4859
+ fs.writeFileSync(absolutePath, "tiny realtime edit");
4860
+ const watcher = makeBatchWatcherStub();
4861
+ const scheduler = {
4862
+ signal: vi.fn(),
4863
+ checkHighWater: vi.fn().mockResolvedValue(undefined),
4864
+ dispose: vi.fn().mockResolvedValue(undefined),
4865
+ };
4866
+ let triggerShutdown = () => { };
4867
+ let releaseBulk = () => { };
4868
+ const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
4869
+ let fullPasses = 0;
4870
+ const runPass = vi.fn().mockImplementation(async (passArgv) => {
4871
+ if (passArgv.includes("--scope-path"))
4872
+ return completedPassOutcome();
4873
+ fullPasses += 1;
4874
+ if (fullPasses === 2)
4875
+ await bulkGate;
4876
+ return completedPassOutcome();
4877
+ });
4878
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "both", "--hq-root", hqRoot], {
4879
+ runPass,
4880
+ clock: new FakeClock(),
4881
+ createWatcher: () => watcher,
4882
+ startRealtimeScheduler: async () => scheduler,
4883
+ sleep: () => new Promise(() => { }),
4884
+ onShutdownSignal: (handler) => {
4885
+ triggerShutdown = handler;
4886
+ return () => { };
4887
+ },
4888
+ });
4889
+ await flushLoopMicrotasks(20);
4890
+ watcher.emit();
4891
+ await flushLoopMicrotasks();
4892
+ scheduler.signal.mockClear();
4893
+ watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
4894
+ await flushLoopMicrotasks(20);
4895
+ expect(scheduler.signal).toHaveBeenCalledWith("watcher");
4896
+ releaseBulk();
4897
+ await flushLoopMicrotasks();
4898
+ triggerShutdown();
4899
+ await loop;
4900
+ fs.rmSync(hqRoot, { recursive: true, force: true });
4901
+ });
4902
+ it("fast lane: applies the receiver cap once across all eligible routes", async () => {
4903
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-receiver-cap-"));
4904
+ const watcher = makeWatcherStub();
4905
+ let receiverSync;
4906
+ let triggerShutdown = () => { };
4907
+ let releaseBulk = () => { };
4908
+ const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
4909
+ let fullPasses = 0;
4910
+ const runPass = vi.fn().mockImplementation(async (passArgv) => {
4911
+ if (passArgv.includes("--scope-path"))
4912
+ return completedPassOutcome();
4913
+ fullPasses += 1;
4914
+ if (fullPasses === 2)
4915
+ await bulkGate;
4916
+ return completedPassOutcome();
4917
+ });
4918
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", hqRoot], {
4919
+ runPass,
4920
+ createWatcher: () => watcher,
4921
+ createReceiver: ({ syncBatchFn }) => {
4922
+ receiverSync = syncBatchFn;
4923
+ return { connected: true, start: async () => { }, dispose: async () => { } };
4924
+ },
4925
+ sleep: () => new Promise(() => { }),
4926
+ onShutdownSignal: (handler) => {
4927
+ triggerShutdown = handler;
4928
+ return () => { };
4929
+ },
4930
+ });
4931
+ await flushLoopMicrotasks();
4932
+ watcher.emit();
4933
+ await flushLoopMicrotasks();
4934
+ runPass.mockClear();
4935
+ const events = [
4936
+ ...Array.from({ length: 17 }, (_, index) => ({
4937
+ kind: "upsert",
4938
+ relativePath: `companies/acme/a-${index}.md`,
4939
+ contentHash: "sha256:x",
4940
+ mtime: "2026-08-26T00:00:00.000Z",
4941
+ originDeviceId: "peer",
4942
+ originTenantId: "acme",
4943
+ sequenceNumber: index,
4944
+ eventTimestamp: "2026-08-26T00:00:00.000Z",
4945
+ })),
4946
+ ...Array.from({ length: 16 }, (_, index) => ({
4947
+ kind: "upsert",
4948
+ relativePath: `companies/beta/b-${index}.md`,
4949
+ contentHash: "sha256:x",
4950
+ mtime: "2026-08-26T00:00:00.000Z",
4951
+ originDeviceId: "peer",
4952
+ originTenantId: "beta",
4953
+ sequenceNumber: index + 17,
4954
+ eventTimestamp: "2026-08-26T00:00:00.000Z",
4955
+ })),
4956
+ ];
4957
+ const receiverDrain = receiverSync({
4958
+ events,
4959
+ signal: new AbortController().signal,
4960
+ });
4961
+ await flushLoopMicrotasks(20);
4962
+ expect(runPass).not.toHaveBeenCalled();
4963
+ releaseBulk();
4964
+ await receiverDrain;
4965
+ triggerShutdown();
4966
+ await loop;
4967
+ fs.rmSync(hqRoot, { recursive: true, force: true });
4968
+ });
4969
+ it("fast lane: queues slow work behind a receiver pass that owns the root lock", async () => {
4970
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-slow-queue-"));
4971
+ const watcher = makeWatcherStub();
4972
+ let receiverSync;
4973
+ let triggerShutdown = () => { };
4974
+ let releaseFast = () => { };
4975
+ let markFastStarted = () => { };
4976
+ const fastGate = new Promise((resolve) => { releaseFast = resolve; });
4977
+ const fastStarted = new Promise((resolve) => { markFastStarted = resolve; });
4978
+ const runPass = vi.fn().mockImplementation(async (passArgv) => {
4979
+ if (passArgv.includes("--scope-path")) {
4980
+ markFastStarted();
4981
+ await fastGate;
4982
+ return completedPassOutcome();
4983
+ }
4984
+ return completedPassOutcome();
4985
+ });
4986
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--lock-timeout", "0", "--hq-root", hqRoot], {
4987
+ runPass,
4988
+ createWatcher: () => watcher,
4989
+ createReceiver: ({ syncBatchFn }) => {
4990
+ receiverSync = syncBatchFn;
4991
+ return { connected: true, start: async () => { }, dispose: async () => { } };
4992
+ },
4993
+ sleep: () => new Promise(() => { }),
4994
+ onShutdownSignal: (handler) => {
4995
+ triggerShutdown = handler;
4996
+ return () => { };
4997
+ },
4998
+ });
4999
+ let loopSettled = false;
5000
+ void loop.then(() => { loopSettled = true; });
5001
+ await flushLoopMicrotasks();
5002
+ const receiverDrain = receiverSync({
5003
+ events: [{
5004
+ kind: "upsert",
5005
+ relativePath: "companies/acme/remote.md",
5006
+ contentHash: "sha256:x",
5007
+ mtime: "2026-08-26T00:00:00.000Z",
5008
+ originDeviceId: "peer",
5009
+ originTenantId: "acme",
5010
+ sequenceNumber: 1,
5011
+ eventTimestamp: "2026-08-26T00:00:00.000Z",
5012
+ }],
5013
+ signal: new AbortController().signal,
5014
+ });
5015
+ await fastStarted;
5016
+ // This wake requests a slow reconcile while the receiver owns the lock.
5017
+ watcher.emit();
5018
+ await flushLoopMicrotasks(20);
5019
+ expect(loopSettled).toBe(false);
5020
+ releaseFast();
5021
+ await receiverDrain;
5022
+ triggerShutdown();
5023
+ await loop;
5024
+ fs.rmSync(hqRoot, { recursive: true, force: true });
5025
+ });
5026
+ it("fast lane: keeps disjoint journal rows under a 16-path interleave with a bulk pass", async () => {
5027
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-journal-"));
5028
+ const paths = Array.from({ length: 16 }, (_, index) => {
5029
+ const relativePath = `companies/indigo/realtime-${index}.md`;
5030
+ const absolutePath = path.join(hqRoot, relativePath);
5031
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
5032
+ fs.writeFileSync(absolutePath, "tiny realtime edit");
5033
+ return [absolutePath, relativePath];
5034
+ });
5035
+ writeJournal("indigo", { version: "2", lastSync: "", pulls: [], files: {} });
5036
+ const watcher = makeBatchWatcherStub();
5037
+ let triggerShutdown = () => { };
5038
+ let releaseBulk = () => { };
5039
+ const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
5040
+ let fullPasses = 0;
5041
+ const runPass = vi.fn().mockImplementation(async (passArgv) => {
5042
+ if (passArgv.includes("--scope-path")) {
5043
+ const fast = readJournal("indigo");
5044
+ for (let index = 0; index < paths.length; index++) {
5045
+ fast.files[`realtime-${index}.md`] = {
5046
+ hash: `fast-${index}`, size: 1, syncedAt: "", direction: "up",
5047
+ };
5048
+ }
5049
+ writeJournal("indigo", fast);
5050
+ return completedPassOutcome();
5051
+ }
5052
+ fullPasses += 1;
5053
+ if (fullPasses === 2) {
5054
+ const slow = readJournal("indigo");
5055
+ await bulkGate;
5056
+ for (let index = 0; index < paths.length; index++) {
5057
+ slow.files[`bulk-${index}.md`] = {
5058
+ hash: `slow-${index}`, size: 1, syncedAt: "", direction: "up",
5059
+ };
5060
+ }
5061
+ writeJournal("indigo", slow);
5062
+ }
5063
+ return completedPassOutcome();
5064
+ });
5065
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "push", "--hq-root", hqRoot], {
5066
+ runPass,
5067
+ clock: new FakeClock(),
5068
+ createWatcher: () => watcher,
5069
+ sleep: () => new Promise(() => { }),
5070
+ onShutdownSignal: (handler) => {
5071
+ triggerShutdown = handler;
5072
+ return () => { };
5073
+ },
5074
+ });
5075
+ await flushLoopMicrotasks();
5076
+ watcher.emit();
5077
+ await flushLoopMicrotasks();
5078
+ watcher.emit(paths[0][1], { paths: new Map(paths) });
5079
+ await flushLoopMicrotasks(30);
5080
+ expect(Object.keys(readJournal("indigo").files)).toHaveLength(paths.length);
5081
+ releaseBulk();
5082
+ await flushLoopMicrotasks();
5083
+ const journal = readJournal("indigo").files;
5084
+ expect(Object.keys(journal)).toHaveLength(paths.length * 2);
5085
+ for (let index = 0; index < paths.length; index++) {
5086
+ expect(journal[`bulk-${index}.md`]?.hash).toBe(`slow-${index}`);
5087
+ expect(journal[`realtime-${index}.md`]?.hash).toBe(`fast-${index}`);
5088
+ }
5089
+ triggerShutdown();
5090
+ await loop;
5091
+ fs.rmSync(hqRoot, { recursive: true, force: true });
5092
+ });
5093
+ it("fast lane: a bulk pass still completes after repeated small watcher batches", async () => {
5094
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-fairness-"));
5095
+ const watcher = makeBatchWatcherStub();
5096
+ let triggerShutdown = () => { };
5097
+ let releaseBulk = () => { };
5098
+ const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
5099
+ const completed = [];
5100
+ let fullPasses = 0;
5101
+ const runPass = vi.fn().mockImplementation(async (passArgv) => {
5102
+ if (passArgv.includes("--scope-path")) {
5103
+ completed.push("fast");
5104
+ return completedPassOutcome();
5105
+ }
5106
+ fullPasses += 1;
5107
+ if (fullPasses === 2) {
5108
+ await bulkGate;
5109
+ completed.push("bulk");
5110
+ }
5111
+ return completedPassOutcome();
5112
+ });
5113
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "push", "--hq-root", hqRoot], {
5114
+ runPass,
5115
+ clock: new FakeClock(),
5116
+ createWatcher: () => watcher,
5117
+ sleep: () => new Promise(() => { }),
5118
+ onShutdownSignal: (handler) => {
5119
+ triggerShutdown = handler;
5120
+ return () => { };
5121
+ },
5122
+ });
5123
+ await flushLoopMicrotasks();
5124
+ watcher.emit();
5125
+ await flushLoopMicrotasks();
5126
+ for (let index = 0; index < 5; index++) {
5127
+ const relativePath = `companies/indigo/realtime-${index}.md`;
5128
+ const absolutePath = path.join(hqRoot, relativePath);
5129
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
5130
+ fs.writeFileSync(absolutePath, "tiny");
5131
+ watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
5132
+ }
5133
+ await flushLoopMicrotasks(60);
5134
+ expect(completed).toEqual(["fast", "fast", "fast", "fast", "fast"]);
5135
+ releaseBulk();
5136
+ await flushLoopMicrotasks();
5137
+ expect(completed).toContain("bulk");
5138
+ triggerShutdown();
5139
+ await loop;
5140
+ fs.rmSync(hqRoot, { recursive: true, force: true });
5141
+ });
5142
+ it("fast lane: sends only batches at or below the file and byte limits", () => {
5143
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-limit-"));
5144
+ const small = path.join(hqRoot, "small.md");
5145
+ const boundary = path.join(hqRoot, "boundary.bin");
5146
+ fs.writeFileSync(small, "small");
5147
+ fs.writeFileSync(boundary, Buffer.alloc(DEFAULT_FAST_LANE_MAX_BYTES));
5148
+ expect(isFastLocalBatch([small])).toBe(true);
5149
+ expect(isFastLocalBatch([boundary])).toBe(true);
5150
+ fs.appendFileSync(boundary, "x");
5151
+ expect(isFastLocalBatch([boundary])).toBe(false);
5152
+ expect(isFastReceiverBatch(DEFAULT_FAST_LANE_MAX_FILES)).toBe(true);
5153
+ expect(isFastReceiverBatch(DEFAULT_FAST_LANE_MAX_FILES + 1)).toBe(false);
5154
+ const previousFiles = process.env[FAST_LANE_MAX_FILES_ENV];
5155
+ const previousBytes = process.env[FAST_LANE_MAX_BYTES_ENV];
5156
+ process.env[FAST_LANE_MAX_FILES_ENV] = "1";
5157
+ process.env[FAST_LANE_MAX_BYTES_ENV] = "4";
5158
+ try {
5159
+ expect(isFastLocalBatch([small])).toBe(false);
5160
+ expect(isFastReceiverBatch(2)).toBe(false);
5161
+ }
5162
+ finally {
5163
+ if (previousFiles === undefined)
5164
+ delete process.env[FAST_LANE_MAX_FILES_ENV];
5165
+ else
5166
+ process.env[FAST_LANE_MAX_FILES_ENV] = previousFiles;
5167
+ if (previousBytes === undefined)
5168
+ delete process.env[FAST_LANE_MAX_BYTES_ENV];
5169
+ else
5170
+ process.env[FAST_LANE_MAX_BYTES_ENV] = previousBytes;
5171
+ fs.rmSync(hqRoot, { recursive: true, force: true });
5172
+ }
5173
+ });
4595
5174
  it("U07: drains the first watcher event after activation without guarded overlap", async () => {
4596
5175
  const watcher = makeWatcherStub();
4597
5176
  let triggerShutdown = () => { };
@@ -4636,8 +5215,9 @@ describe("runRunnerWithLoop — event-push wiring", () => {
4636
5215
  // before the next wait is registered. Its first event must still drain.
4637
5216
  watcher.emit("companies/indigo/pre-registration.md");
4638
5217
  await flushLoopMicrotasks();
4639
- expect(spans).toHaveLength(2);
5218
+ expect(spans).toHaveLength(3);
4640
5219
  expect(spans[1].enteredAt >= spans[0].exitedAt).toBe(true);
5220
+ expect(spans[2].enteredAt >= spans[1].exitedAt).toBe(true);
4641
5221
  triggerShutdown();
4642
5222
  await loop;
4643
5223
  });
@@ -5146,6 +5726,8 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
5146
5726
  "bare-wake": matched.includes("bare-wake"),
5147
5727
  "batch-over-limit": matched.includes("batch-over-limit"),
5148
5728
  "overflow-routes-unknown": matched.includes("overflow-routes-unknown"),
5729
+ "watcher-warmup-catch-up": matched.includes("watcher-warmup-catch-up"),
5730
+ "watcher-warmup-catch-up-failed": matched.includes("watcher-warmup-catch-up-failed"),
5149
5731
  "scheduled-interval": matched.includes("scheduled-interval"),
5150
5732
  });
5151
5733
  function expectFullReconcileDecision(record, mode, reasons) {
@@ -5518,12 +6100,11 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
5518
6100
  tick();
5519
6101
  await loop;
5520
6102
  });
5521
- it("omitted --direction is pull-only (parser default): a scoped tick never pushes", async () => {
6103
+ it("a pending local batch is pushed on a pull-driven watcher tick", async () => {
5522
6104
  const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
5523
- // No --direction flag → parseArgs defaults the run to pull-only. The drain
5524
- // must honor that and NOT run a scoped push. Regression: the loop used to
5525
- // re-parse argv and wrongly default an omitted direction to "both", so a
5526
- // pull-only watcher would push local edits it should never send.
6105
+ // No --direction flag → parseArgs defaults the cadence run to pull-only.
6106
+ // A watcher batch remains an explicit local-write delivery request: its
6107
+ // bounded push must run before the pull leg can consume the batch.
5527
6108
  const pullOnlyArgv = [
5528
6109
  "--companies",
5529
6110
  "--watch",
@@ -5540,11 +6121,19 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
5540
6121
  tick();
5541
6122
  await flushLoopMicrotasks();
5542
6123
  const calls = runPass.mock.calls.map((call) => call[0]);
5543
- // No scoped push of any kind ran...
5544
- expect(calls.some((argv) => argv.includes("--direction") &&
5545
- argv[argv.indexOf("--direction") + 1] === "push")).toBe(false);
5546
- // ...only the pull leg ran — route-targeted and path-scoped now.
6124
+ // This expectation fails on origin/main: the old pull-only branch runs
6125
+ // only the second argv, so the local write waits for a later reconcile.
5547
6126
  expect(calls).toEqual([
6127
+ [
6128
+ "--company",
6129
+ "indigo",
6130
+ "--direction",
6131
+ "push",
6132
+ "--scope-path",
6133
+ "knowledge/a.md",
6134
+ "--hq-root",
6135
+ "/tmp/hq",
6136
+ ],
5548
6137
  [
5549
6138
  "--company",
5550
6139
  "indigo",
@@ -5560,6 +6149,98 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
5560
6149
  tick();
5561
6150
  await loop;
5562
6151
  });
6152
+ it("pushes a local event in the pass immediately after a guarded receiver drain", async () => {
6153
+ const watcher = makeBatchWatcherStub();
6154
+ const sleep = makeSteppableSleep();
6155
+ let shutdown = () => { };
6156
+ let receiverSync = null;
6157
+ let releaseReceiverPull = () => { };
6158
+ const receiverPull = new Promise((resolve) => {
6159
+ releaseReceiverPull = resolve;
6160
+ });
6161
+ const runPass = vi.fn(async (argv) => {
6162
+ if (argv.includes("--company") &&
6163
+ argv.includes("acme") &&
6164
+ argv[argv.indexOf("--direction") + 1] === "pull") {
6165
+ await receiverPull;
6166
+ }
6167
+ return completedPassOutcome();
6168
+ });
6169
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
6170
+ runPass,
6171
+ createWatcher: () => watcher,
6172
+ createReceiver: ({ syncBatchFn }) => {
6173
+ receiverSync = syncBatchFn;
6174
+ return { connected: true, start: async () => { }, dispose: async () => { } };
6175
+ },
6176
+ sleep: sleep.sleep,
6177
+ onShutdownSignal: (handler) => {
6178
+ shutdown = handler;
6179
+ return () => { };
6180
+ },
6181
+ });
6182
+ await flushLoopMicrotasks(12);
6183
+ runPass.mockClear();
6184
+ const receiverDrain = receiverSync({
6185
+ events: [{
6186
+ kind: "upsert",
6187
+ relativePath: "companies/acme/remote.md",
6188
+ contentHash: "sha256:remote",
6189
+ mtime: "2026-08-26T08:00:00.000Z",
6190
+ originDeviceId: "peer-device",
6191
+ originTenantId: "tenant-acme",
6192
+ sequenceNumber: 1,
6193
+ eventTimestamp: "2026-08-26T08:00:00.000Z",
6194
+ }],
6195
+ signal: new AbortController().signal,
6196
+ });
6197
+ await flushLoopMicrotasks();
6198
+ watcher.emit("companies/indigo/local.md", {
6199
+ paths: new Map([
6200
+ ["/tmp/hq/companies/indigo/local.md", "companies/indigo/local.md"],
6201
+ ]),
6202
+ });
6203
+ await flushLoopMicrotasks();
6204
+ releaseReceiverPull();
6205
+ await receiverDrain;
6206
+ await flushLoopMicrotasks(16);
6207
+ // On origin/main the next call is an indigo pull, so this asserts the
6208
+ // bounded local upload is not consumed by the receiver/drain cycle.
6209
+ expect(runPass.mock.calls.map((call) => call[0])).toEqual([
6210
+ [
6211
+ "--company",
6212
+ "acme",
6213
+ "--direction",
6214
+ "pull",
6215
+ "--scope-path",
6216
+ "remote.md",
6217
+ "--hq-root",
6218
+ "/tmp/hq",
6219
+ ],
6220
+ [
6221
+ "--company",
6222
+ "indigo",
6223
+ "--direction",
6224
+ "push",
6225
+ "--scope-path",
6226
+ "local.md",
6227
+ "--hq-root",
6228
+ "/tmp/hq",
6229
+ ],
6230
+ [
6231
+ "--company",
6232
+ "indigo",
6233
+ "--direction",
6234
+ "pull",
6235
+ "--scope-path",
6236
+ "local.md",
6237
+ "--hq-root",
6238
+ "/tmp/hq",
6239
+ ],
6240
+ ]);
6241
+ shutdown();
6242
+ await loop;
6243
+ });
5563
6244
  it("restores failed scoped push paths so the next tick retries them", async () => {
5564
6245
  const runPass = vi
5565
6246
  .fn()
@@ -6975,15 +7656,14 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6975
7656
  const sleep = makeSteppableSleep();
6976
7657
  let triggerShutdown = () => { };
6977
7658
  const loop = runRunnerWithLoop(
6978
- // `--direction both` so the push leg (and therefore the publish-after-push
6979
- // wiring these tests exercise) actually runs. Omitting --direction is a
6980
- // pull-only run by the parser default, which correctly never pushes.
7659
+ // Most wiring tests use `both`; pull-only watcher tests pin that a local
7660
+ // event still takes its scoped upload and publishes after success.
6981
7661
  [
6982
7662
  "--companies",
6983
7663
  "--watch",
6984
7664
  "--event-push",
6985
7665
  "--direction",
6986
- "both",
7666
+ opts.direction ?? "both",
6987
7667
  "--hq-root",
6988
7668
  "/tmp/hq",
6989
7669
  ], {
@@ -7136,6 +7816,33 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
7136
7816
  shutdown();
7137
7817
  await loop;
7138
7818
  });
7819
+ it("publishes exactly one event per path after a pull-only watcher upload", async () => {
7820
+ const publishBatch = vi.fn();
7821
+ const startEventSync = vi.fn().mockResolvedValue({
7822
+ publishBatch,
7823
+ receiver: { start: async () => { }, dispose: async () => { }, connected: true },
7824
+ ownDeviceId: "dev-test",
7825
+ dispose: vi.fn().mockResolvedValue(undefined),
7826
+ });
7827
+ const { loop, watcher, clock, shutdown } = runLoop({
7828
+ claims: EMAIL_CLAIMS,
7829
+ direction: "pull",
7830
+ startEventSync,
7831
+ });
7832
+ await microtasks();
7833
+ const relativePath = "companies/indigo/local.md";
7834
+ watcher.emit(relativePath, {
7835
+ paths: new Map([[`/tmp/hq/${relativePath}`, relativePath]]),
7836
+ });
7837
+ clock.advance(0);
7838
+ await microtasks();
7839
+ // The old pull-only branch never invoked the scoped push, so it published
7840
+ // zero events here. One accepted local upload must announce its path once.
7841
+ expect(publishBatch).toHaveBeenCalledTimes(1);
7842
+ expect([...publishBatch.mock.calls[0][0].paths.values()]).toEqual([relativePath]);
7843
+ shutdown();
7844
+ await loop;
7845
+ });
7139
7846
  it("publishes a personal-vault edit from a --companies event-push runner", async () => {
7140
7847
  const publishBatch = vi.fn();
7141
7848
  const startEventSync = vi.fn().mockResolvedValue({