@indigoai-us/hq-cloud 6.15.31 → 6.15.32

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 (125) hide show
  1. package/dist/bandwidth.d.ts +128 -0
  2. package/dist/bandwidth.d.ts.map +1 -0
  3. package/dist/bandwidth.js +254 -0
  4. package/dist/bandwidth.js.map +1 -0
  5. package/dist/bandwidth.test.d.ts +2 -0
  6. package/dist/bandwidth.test.d.ts.map +1 -0
  7. package/dist/bandwidth.test.js +243 -0
  8. package/dist/bandwidth.test.js.map +1 -0
  9. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  10. package/dist/bin/sync-runner-company.js +13 -24
  11. package/dist/bin/sync-runner-company.js.map +1 -1
  12. package/dist/bin/sync-runner-company.test.js +71 -0
  13. package/dist/bin/sync-runner-company.test.js.map +1 -1
  14. package/dist/bin/sync-runner-watch-loop.d.ts +10 -0
  15. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  16. package/dist/bin/sync-runner-watch-loop.js +181 -16
  17. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  18. package/dist/bin/sync-runner-watch-routes.d.ts +16 -1
  19. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
  20. package/dist/bin/sync-runner-watch-routes.js +40 -2
  21. package/dist/bin/sync-runner-watch-routes.js.map +1 -1
  22. package/dist/bin/sync-runner-watch-routes.test.js +53 -1
  23. package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
  24. package/dist/bin/sync-runner.d.ts +15 -3
  25. package/dist/bin/sync-runner.d.ts.map +1 -1
  26. package/dist/bin/sync-runner.js +66 -13
  27. package/dist/bin/sync-runner.js.map +1 -1
  28. package/dist/bin/sync-runner.test.js +433 -13
  29. package/dist/bin/sync-runner.test.js.map +1 -1
  30. package/dist/cli/conflict.d.ts +25 -2
  31. package/dist/cli/conflict.d.ts.map +1 -1
  32. package/dist/cli/conflict.js +29 -4
  33. package/dist/cli/conflict.js.map +1 -1
  34. package/dist/cli/share.d.ts +40 -4
  35. package/dist/cli/share.d.ts.map +1 -1
  36. package/dist/cli/share.js +478 -86
  37. package/dist/cli/share.js.map +1 -1
  38. package/dist/cli/share.test.js +845 -150
  39. package/dist/cli/share.test.js.map +1 -1
  40. package/dist/cli/sync-scope.test.js +147 -18
  41. package/dist/cli/sync-scope.test.js.map +1 -1
  42. package/dist/cli/sync.d.ts +20 -0
  43. package/dist/cli/sync.d.ts.map +1 -1
  44. package/dist/cli/sync.js +261 -29
  45. package/dist/cli/sync.js.map +1 -1
  46. package/dist/cli/sync.test.js +519 -90
  47. package/dist/cli/sync.test.js.map +1 -1
  48. package/dist/journal-delta-writes.test.d.ts +2 -0
  49. package/dist/journal-delta-writes.test.d.ts.map +1 -0
  50. package/dist/journal-delta-writes.test.js +140 -0
  51. package/dist/journal-delta-writes.test.js.map +1 -0
  52. package/dist/journal.d.ts +1 -0
  53. package/dist/journal.d.ts.map +1 -1
  54. package/dist/journal.js +155 -25
  55. package/dist/journal.js.map +1 -1
  56. package/dist/journal.test.js +29 -11
  57. package/dist/journal.test.js.map +1 -1
  58. package/dist/object-io.d.ts +34 -13
  59. package/dist/object-io.d.ts.map +1 -1
  60. package/dist/object-io.js +154 -29
  61. package/dist/object-io.js.map +1 -1
  62. package/dist/operation-lock.d.ts +12 -0
  63. package/dist/operation-lock.d.ts.map +1 -1
  64. package/dist/operation-lock.js +103 -21
  65. package/dist/operation-lock.js.map +1 -1
  66. package/dist/operation-lock.test.js +90 -0
  67. package/dist/operation-lock.test.js.map +1 -1
  68. package/dist/outcome-telemetry.d.ts +7 -1
  69. package/dist/outcome-telemetry.d.ts.map +1 -1
  70. package/dist/outcome-telemetry.js +15 -9
  71. package/dist/outcome-telemetry.js.map +1 -1
  72. package/dist/outcome-telemetry.test.js +15 -0
  73. package/dist/outcome-telemetry.test.js.map +1 -1
  74. package/dist/outposts/client.d.ts +2 -2
  75. package/dist/outposts/client.d.ts.map +1 -1
  76. package/dist/outposts/client.js +4 -3
  77. package/dist/outposts/client.js.map +1 -1
  78. package/dist/outposts/command.d.ts.map +1 -1
  79. package/dist/outposts/command.js +9 -6
  80. package/dist/outposts/command.js.map +1 -1
  81. package/dist/outposts/command.test.js +59 -2
  82. package/dist/outposts/command.test.js.map +1 -1
  83. package/dist/outposts/remote-command.d.ts +8 -8
  84. package/dist/outposts/remote-command.d.ts.map +1 -1
  85. package/dist/outposts/remote-command.js +10 -8
  86. package/dist/outposts/remote-command.js.map +1 -1
  87. package/dist/prefix-coalesce.d.ts +14 -0
  88. package/dist/prefix-coalesce.d.ts.map +1 -1
  89. package/dist/prefix-coalesce.js +29 -0
  90. package/dist/prefix-coalesce.js.map +1 -1
  91. package/dist/prefix-coalesce.test.js +22 -1
  92. package/dist/prefix-coalesce.test.js.map +1 -1
  93. package/dist/skill-telemetry.d.ts +6 -1
  94. package/dist/skill-telemetry.d.ts.map +1 -1
  95. package/dist/skill-telemetry.js +14 -8
  96. package/dist/skill-telemetry.js.map +1 -1
  97. package/dist/skill-telemetry.test.js +43 -0
  98. package/dist/skill-telemetry.test.js.map +1 -1
  99. package/dist/sts-credential-refresh.test.js +87 -2
  100. package/dist/sts-credential-refresh.test.js.map +1 -1
  101. package/dist/sync/state-store.d.ts.map +1 -1
  102. package/dist/sync/state-store.js +20 -1
  103. package/dist/sync/state-store.js.map +1 -1
  104. package/dist/sync/state-store.test.js +64 -0
  105. package/dist/sync/state-store.test.js.map +1 -1
  106. package/dist/sync-core.d.ts.map +1 -1
  107. package/dist/sync-core.js +7 -2
  108. package/dist/sync-core.js.map +1 -1
  109. package/dist/sync-core.test.js +36 -0
  110. package/dist/sync-core.test.js.map +1 -1
  111. package/dist/telemetry.d.ts +30 -6
  112. package/dist/telemetry.d.ts.map +1 -1
  113. package/dist/telemetry.js +331 -135
  114. package/dist/telemetry.js.map +1 -1
  115. package/dist/telemetry.test.js +403 -0
  116. package/dist/telemetry.test.js.map +1 -1
  117. package/dist/types.d.ts +26 -3
  118. package/dist/types.d.ts.map +1 -1
  119. package/dist/watcher.d.ts +76 -0
  120. package/dist/watcher.d.ts.map +1 -1
  121. package/dist/watcher.js +105 -3
  122. package/dist/watcher.js.map +1 -1
  123. package/dist/watcher.test.js +165 -1
  124. package/dist/watcher.test.js.map +1 -1
  125. package/package.json +1 -1
@@ -12,6 +12,7 @@ import * as os from "os";
12
12
  import * as path from "path";
13
13
  import { runRunner, runRunnerWithLoop, resolveDeletePolicy, resolveSkipPersonal, resolveSkipCompanies, selectObjectIOFactory, routeChangeToTarget, buildTargetedPushArgv, resolvePullScope, readPinnedPrefixes, SESSIONS_SCOPE_PREFIX, defaultCollectTelemetry, AUTH_REQUIRED_PASS_EXIT, } from "./sync-runner.js";
14
14
  import { PresignObjectIO, S3SdkObjectIO } from "../object-io.js";
15
+ import { setBandwidthGovernorForTesting } from "../bandwidth.js";
15
16
  import { FakeClock, } from "../watcher.js";
16
17
  import { adaptivePollMs, advanceMonotonicDeadline, journalEntriesForWatcherDelete, shouldQuarantineWatcherDeleteIntents, } from "./sync-runner-watch-loop.js";
17
18
  import { PERSONAL_VAULT_JOURNAL_SLUG, readJournal, writeJournal, } from "../journal.js";
@@ -215,6 +216,63 @@ describe("argv parsing", () => {
215
216
  });
216
217
  });
217
218
  // ---------------------------------------------------------------------------
219
+ // bandwidth governor startup line
220
+ // ---------------------------------------------------------------------------
221
+ describe("bandwidth governor startup line", () => {
222
+ const ENV_KEYS = [
223
+ "HQ_SYNC_MAX_BYTES_PER_SEC",
224
+ "HQ_SYNC_MAX_CONCURRENCY",
225
+ "HQ_SYNC_BANDWIDTH_PERCENT",
226
+ ];
227
+ let saved;
228
+ beforeEach(() => {
229
+ saved = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
230
+ for (const k of ENV_KEYS)
231
+ delete process.env[k];
232
+ setBandwidthGovernorForTesting(null); // re-read env per test
233
+ });
234
+ afterEach(() => {
235
+ for (const k of ENV_KEYS) {
236
+ if (saved[k] === undefined)
237
+ delete process.env[k];
238
+ else
239
+ process.env[k] = saved[k];
240
+ }
241
+ setBandwidthGovernorForTesting(null);
242
+ });
243
+ it("states the effective policy on stderr when the env vars are set", async () => {
244
+ process.env.HQ_SYNC_MAX_BYTES_PER_SEC = "500000";
245
+ process.env.HQ_SYNC_MAX_CONCURRENCY = "2";
246
+ const deps = makeDeps({
247
+ createVaultClient: () => makeVaultStub({ memberships: [] }),
248
+ });
249
+ const code = await runRunner(["--companies"], deps);
250
+ expect(code).toBe(0);
251
+ expect(deps.stderr.events()).toContainEqual({
252
+ type: "bandwidth-policy",
253
+ message: "bandwidth governor: 500000 B/s cap, concurrency 2",
254
+ });
255
+ });
256
+ it("stays silent (exact legacy stream) when no policy is configured", async () => {
257
+ const deps = makeDeps({
258
+ createVaultClient: () => makeVaultStub({ memberships: [] }),
259
+ });
260
+ const code = await runRunner(["--companies"], deps);
261
+ expect(code).toBe(0);
262
+ expect(deps.stderr.events().some((e) => e.type === "bandwidth-policy")).toBe(false);
263
+ });
264
+ it("treats invalid env values as off (no policy line)", async () => {
265
+ process.env.HQ_SYNC_MAX_BYTES_PER_SEC = "0";
266
+ process.env.HQ_SYNC_BANDWIDTH_PERCENT = "101";
267
+ const deps = makeDeps({
268
+ createVaultClient: () => makeVaultStub({ memberships: [] }),
269
+ });
270
+ const code = await runRunner(["--companies"], deps);
271
+ expect(code).toBe(0);
272
+ expect(deps.stderr.events().some((e) => e.type === "bandwidth-policy")).toBe(false);
273
+ });
274
+ });
275
+ // ---------------------------------------------------------------------------
218
276
  // auth
219
277
  // ---------------------------------------------------------------------------
220
278
  describe("auth", () => {
@@ -4523,7 +4581,18 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4523
4581
  "--hq-root",
4524
4582
  "/tmp/hq",
4525
4583
  ],
4526
- fullPullArgv,
4584
+ // The drain pull is route-targeted AND path-scoped now: it lists only
4585
+ // the touched subtree instead of running a full --companies pull.
4586
+ [
4587
+ "--company",
4588
+ "indigo",
4589
+ "--direction",
4590
+ "pull",
4591
+ "--scope-path",
4592
+ "knowledge/a.md",
4593
+ "--hq-root",
4594
+ "/tmp/hq",
4595
+ ],
4527
4596
  ]);
4528
4597
  shutdown();
4529
4598
  tick();
@@ -4540,7 +4609,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4540
4609
  await flushLoopMicrotasks();
4541
4610
  const pushCalls = runPass.mock.calls
4542
4611
  .map((call) => call[0])
4543
- .filter((argv) => argv.includes("--company"));
4612
+ .filter((argv) => argv.includes("--company") && argv.includes("push"));
4544
4613
  expect(pushCalls).toEqual([
4545
4614
  [
4546
4615
  "--company",
@@ -4620,11 +4689,21 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4620
4689
  await flushLoopMicrotasks();
4621
4690
  const calls = runPass.mock.calls.map((call) => call[0]);
4622
4691
  // No scoped push of any kind ran...
4623
- expect(calls.some((argv) => argv.includes("--company"))).toBe(false);
4624
4692
  expect(calls.some((argv) => argv.includes("--direction") &&
4625
4693
  argv[argv.indexOf("--direction") + 1] === "push")).toBe(false);
4626
- // ...only the pull leg ran.
4627
- expect(calls).toEqual([fullPullArgv]);
4694
+ // ...only the pull leg ran — route-targeted and path-scoped now.
4695
+ expect(calls).toEqual([
4696
+ [
4697
+ "--company",
4698
+ "indigo",
4699
+ "--direction",
4700
+ "pull",
4701
+ "--scope-path",
4702
+ "knowledge/a.md",
4703
+ "--hq-root",
4704
+ "/tmp/hq",
4705
+ ],
4706
+ ]);
4628
4707
  shutdown();
4629
4708
  tick();
4630
4709
  await loop;
@@ -4647,7 +4726,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4647
4726
  await flushLoopMicrotasks();
4648
4727
  const pushCalls = runPass.mock.calls
4649
4728
  .map((call) => call[0])
4650
- .filter((argv) => argv.includes("--company"));
4729
+ .filter((argv) => argv.includes("--company") && argv.includes("push"));
4651
4730
  expect(pushCalls).toEqual([
4652
4731
  [
4653
4732
  "--company",
@@ -4714,7 +4793,17 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4714
4793
  "--hq-root",
4715
4794
  "/tmp/hq",
4716
4795
  ],
4717
- ["--personal", "--direction", "pull", "--hq-root", "/tmp/hq"],
4796
+ // Path-scoped now: the personal pull leg lists only the touched
4797
+ // subtree instead of re-walking the whole vault on every event batch.
4798
+ [
4799
+ "--personal",
4800
+ "--direction",
4801
+ "pull",
4802
+ "--scope-path",
4803
+ "knowledge/a.md",
4804
+ "--hq-root",
4805
+ "/tmp/hq",
4806
+ ],
4718
4807
  ]);
4719
4808
  shutdown();
4720
4809
  tick();
@@ -4750,7 +4839,16 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4750
4839
  "--hq-root",
4751
4840
  "/tmp/hq",
4752
4841
  ],
4753
- ["--company", "acme", "--direction", "pull", "--hq-root", "/tmp/hq"],
4842
+ [
4843
+ "--company",
4844
+ "acme",
4845
+ "--direction",
4846
+ "pull",
4847
+ "--scope-path",
4848
+ "knowledge/a.md",
4849
+ "--hq-root",
4850
+ "/tmp/hq",
4851
+ ],
4754
4852
  ]);
4755
4853
  shutdown();
4756
4854
  tick();
@@ -4828,6 +4926,245 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4828
4926
  sleep.tick();
4829
4927
  await loop;
4830
4928
  });
4929
+ it("overflow with known dropped-route hints fans out per touched route instead of a full reconcile", async () => {
4930
+ const watcher = makeBatchWatcherStub();
4931
+ const sleep = makeSteppableSleep();
4932
+ let triggerShutdown = () => { };
4933
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4934
+ const loop = runRunnerWithLoop(watchArgv, {
4935
+ runPass,
4936
+ clock: new FakeClock(),
4937
+ createWatcher: () => watcher,
4938
+ sleep: sleep.sleep,
4939
+ onShutdownSignal: (handler) => {
4940
+ triggerShutdown = handler;
4941
+ return () => { };
4942
+ },
4943
+ });
4944
+ await flushLoopMicrotasks();
4945
+ runPass.mockClear();
4946
+ // A burst overflowed, but every dropped path recorded route evidence:
4947
+ // retained path in indigo, dropped paths in acme. The drain must run one
4948
+ // whole-route pass per touched route — never the all-companies fanout.
4949
+ watcher.emit("companies/indigo/a.md", {
4950
+ paths: new Map([
4951
+ ["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"],
4952
+ ]),
4953
+ overflowed: true,
4954
+ droppedPaths: 3,
4955
+ droppedRouteHints: ["companies/acme/dropped.md"],
4956
+ });
4957
+ sleep.tick();
4958
+ // Four guarded passes run sequentially — give the loop room to finish.
4959
+ await flushLoopMicrotasks(40);
4960
+ const calls = runPass.mock.calls.map((call) => call[0]);
4961
+ expect(calls).toEqual([
4962
+ // indigo: whole-route push (no --scope-path — dropped paths unknown)…
4963
+ ["--company", "indigo", "--direction", "push", "--hq-root", "/tmp/hq"],
4964
+ // …then the route's pull.
4965
+ ["--company", "indigo", "--direction", "pull", "--hq-root", "/tmp/hq"],
4966
+ ["--company", "acme", "--direction", "push", "--hq-root", "/tmp/hq"],
4967
+ ["--company", "acme", "--direction", "pull", "--hq-root", "/tmp/hq"],
4968
+ ]);
4969
+ expect(calls).not.toContainEqual(fullArgv);
4970
+ triggerShutdown();
4971
+ await loop;
4972
+ });
4973
+ it("falls back to a full reconcile when an event batch exceeds HQ_SYNC_EVENT_BATCH_LIMIT", async () => {
4974
+ const saved = process.env.HQ_SYNC_EVENT_BATCH_LIMIT;
4975
+ process.env.HQ_SYNC_EVENT_BATCH_LIMIT = "2";
4976
+ try {
4977
+ const watcher = makeBatchWatcherStub();
4978
+ const sleep = makeSteppableSleep();
4979
+ let triggerShutdown = () => { };
4980
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4981
+ const loop = runRunnerWithLoop(watchArgv, {
4982
+ runPass,
4983
+ clock: new FakeClock(),
4984
+ createWatcher: () => watcher,
4985
+ sleep: sleep.sleep,
4986
+ onShutdownSignal: (handler) => {
4987
+ triggerShutdown = handler;
4988
+ return () => { };
4989
+ },
4990
+ });
4991
+ await flushLoopMicrotasks();
4992
+ runPass.mockClear();
4993
+ watcher.emit("companies/indigo/a.md", {
4994
+ paths: new Map([
4995
+ ["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"],
4996
+ ["/tmp/hq/companies/indigo/b.md", "companies/indigo/b.md"],
4997
+ ["/tmp/hq/companies/indigo/c.md", "companies/indigo/c.md"],
4998
+ ]),
4999
+ });
5000
+ sleep.tick();
5001
+ await flushLoopMicrotasks();
5002
+ // Mass change: one full reconcile, no per-path scoped passes.
5003
+ expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
5004
+ triggerShutdown();
5005
+ await loop;
5006
+ }
5007
+ finally {
5008
+ if (saved === undefined)
5009
+ delete process.env.HQ_SYNC_EVENT_BATCH_LIMIT;
5010
+ else
5011
+ process.env.HQ_SYNC_EVENT_BATCH_LIMIT = saved;
5012
+ }
5013
+ });
5014
+ it("a batch under the default limit stays on the targeted path", async () => {
5015
+ const watcher = makeBatchWatcherStub();
5016
+ const sleep = makeSteppableSleep();
5017
+ let triggerShutdown = () => { };
5018
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
5019
+ const loop = runRunnerWithLoop(watchArgv, {
5020
+ runPass,
5021
+ clock: new FakeClock(),
5022
+ createWatcher: () => watcher,
5023
+ sleep: sleep.sleep,
5024
+ onShutdownSignal: (handler) => {
5025
+ triggerShutdown = handler;
5026
+ return () => { };
5027
+ },
5028
+ });
5029
+ await flushLoopMicrotasks();
5030
+ runPass.mockClear();
5031
+ watcher.emit("companies/indigo/a.md", {
5032
+ paths: new Map([
5033
+ ["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"],
5034
+ ["/tmp/hq/companies/indigo/b.md", "companies/indigo/b.md"],
5035
+ ["/tmp/hq/companies/indigo/c.md", "companies/indigo/c.md"],
5036
+ ]),
5037
+ });
5038
+ sleep.tick();
5039
+ await flushLoopMicrotasks();
5040
+ const calls = runPass.mock.calls.map((call) => call[0]);
5041
+ expect(calls).not.toContainEqual(fullArgv);
5042
+ expect(calls[0]).toEqual([
5043
+ "--company",
5044
+ "indigo",
5045
+ "--direction",
5046
+ "push",
5047
+ "--scope-path",
5048
+ "a.md",
5049
+ "--scope-path",
5050
+ "b.md",
5051
+ "--scope-path",
5052
+ "c.md",
5053
+ "--hq-root",
5054
+ "/tmp/hq",
5055
+ ]);
5056
+ triggerShutdown();
5057
+ await loop;
5058
+ });
5059
+ it("a throwing targeted pass falls back to one full reconcile instead of dropping the change", async () => {
5060
+ const runPass = vi.fn(async (argv) => {
5061
+ if (argv.includes("pull") && argv.includes("--scope-path")) {
5062
+ throw new Error("scoped pull exploded");
5063
+ }
5064
+ return completedPassOutcome();
5065
+ });
5066
+ const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
5067
+ await flushLoopMicrotasks();
5068
+ runPass.mockClear();
5069
+ watcher.emit("companies/indigo/knowledge/a.md");
5070
+ tick();
5071
+ await flushLoopMicrotasks(40);
5072
+ expect(runPass.mock.calls.map((call) => call[0])).toEqual([
5073
+ [
5074
+ "--company",
5075
+ "indigo",
5076
+ "--direction",
5077
+ "push",
5078
+ "--scope-path",
5079
+ "knowledge/a.md",
5080
+ "--hq-root",
5081
+ "/tmp/hq",
5082
+ ],
5083
+ // The scoped pull throws…
5084
+ [
5085
+ "--company",
5086
+ "indigo",
5087
+ "--direction",
5088
+ "pull",
5089
+ "--scope-path",
5090
+ "knowledge/a.md",
5091
+ "--hq-root",
5092
+ "/tmp/hq",
5093
+ ],
5094
+ // …and the loop degrades to the full reconcile, never dropping the tick.
5095
+ fullArgv,
5096
+ ]);
5097
+ shutdown();
5098
+ tick();
5099
+ await loop;
5100
+ });
5101
+ it("wires the event debounce config (default 15s quiet / 120s max-wait) into the watcher factory", async () => {
5102
+ const captured = [];
5103
+ const watcher = makeWatcherStub();
5104
+ const sleep = makeSteppableSleep();
5105
+ let triggerShutdown = () => { };
5106
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
5107
+ const loop = runRunnerWithLoop(watchArgv, {
5108
+ runPass,
5109
+ clock: new FakeClock(),
5110
+ createWatcher: (opts) => {
5111
+ captured.push({ debounceMs: opts.debounceMs, maxWaitMs: opts.maxWaitMs });
5112
+ return watcher;
5113
+ },
5114
+ sleep: sleep.sleep,
5115
+ onShutdownSignal: (handler) => {
5116
+ triggerShutdown = handler;
5117
+ return () => { };
5118
+ },
5119
+ });
5120
+ await flushLoopMicrotasks();
5121
+ expect(captured).toEqual([{ debounceMs: 15_000, maxWaitMs: 120_000 }]);
5122
+ triggerShutdown();
5123
+ await loop;
5124
+ });
5125
+ it("HQ_SYNC_EVENT_DEBOUNCE_MS / HQ_SYNC_EVENT_MAX_WAIT_MS override the watcher debounce", async () => {
5126
+ const savedDebounce = process.env.HQ_SYNC_EVENT_DEBOUNCE_MS;
5127
+ const savedMaxWait = process.env.HQ_SYNC_EVENT_MAX_WAIT_MS;
5128
+ process.env.HQ_SYNC_EVENT_DEBOUNCE_MS = "5000";
5129
+ process.env.HQ_SYNC_EVENT_MAX_WAIT_MS = "45000";
5130
+ try {
5131
+ const captured = [];
5132
+ const watcher = makeWatcherStub();
5133
+ const sleep = makeSteppableSleep();
5134
+ let triggerShutdown = () => { };
5135
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
5136
+ const loop = runRunnerWithLoop(watchArgv, {
5137
+ runPass,
5138
+ clock: new FakeClock(),
5139
+ createWatcher: (opts) => {
5140
+ captured.push({
5141
+ debounceMs: opts.debounceMs,
5142
+ maxWaitMs: opts.maxWaitMs,
5143
+ });
5144
+ return watcher;
5145
+ },
5146
+ sleep: sleep.sleep,
5147
+ onShutdownSignal: (handler) => {
5148
+ triggerShutdown = handler;
5149
+ return () => { };
5150
+ },
5151
+ });
5152
+ await flushLoopMicrotasks();
5153
+ expect(captured).toEqual([{ debounceMs: 5000, maxWaitMs: 45_000 }]);
5154
+ triggerShutdown();
5155
+ await loop;
5156
+ }
5157
+ finally {
5158
+ if (savedDebounce === undefined)
5159
+ delete process.env.HQ_SYNC_EVENT_DEBOUNCE_MS;
5160
+ else
5161
+ process.env.HQ_SYNC_EVENT_DEBOUNCE_MS = savedDebounce;
5162
+ if (savedMaxWait === undefined)
5163
+ delete process.env.HQ_SYNC_EVENT_MAX_WAIT_MS;
5164
+ else
5165
+ process.env.HQ_SYNC_EVENT_MAX_WAIT_MS = savedMaxWait;
5166
+ }
5167
+ });
4831
5168
  });
4832
5169
  // ---------------------------------------------------------------------------
4833
5170
  // Re-initialize for each test (mock state hygiene)
@@ -6050,7 +6387,17 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6050
6387
  "--hq-root",
6051
6388
  "/tmp/hq",
6052
6389
  ],
6053
- ["--company", "acme", "--direction", "pull", "--hq-root", "/tmp/hq"],
6390
+ // The receiver's targeted pull is path-scoped to the pushed key now.
6391
+ [
6392
+ "--company",
6393
+ "acme",
6394
+ "--direction",
6395
+ "pull",
6396
+ "--scope-path",
6397
+ "remote.md",
6398
+ "--hq-root",
6399
+ "/tmp/hq",
6400
+ ],
6054
6401
  ]));
6055
6402
  const pushedRoutes = new Set();
6056
6403
  for (const passArgv of passArgvs) {
@@ -6151,7 +6498,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6151
6498
  // the differential signal for "did telemetry run".
6152
6499
  // ---------------------------------------------------------------------------
6153
6500
  describe("defaultCollectTelemetry person-entity gate", () => {
6154
- function makeTelemetryClient(persons) {
6501
+ function makeTelemetryClient(persons, agents = []) {
6155
6502
  const calls = {
6156
6503
  listByType: 0,
6157
6504
  getTelemetryOptIn: 0,
@@ -6161,9 +6508,9 @@ describe("defaultCollectTelemetry person-entity gate", () => {
6161
6508
  const client = {
6162
6509
  entity: {
6163
6510
  get: async () => ({}),
6164
- listByType: async (_type) => {
6511
+ listByType: async (type) => {
6165
6512
  calls.listByType++;
6166
- return persons;
6513
+ return type === "agent" ? agents : persons;
6167
6514
  },
6168
6515
  },
6169
6516
  // Telemetry surface (cast-to internally by defaultCollectTelemetry). With
@@ -6198,7 +6545,7 @@ describe("defaultCollectTelemetry person-entity gate", () => {
6198
6545
  try {
6199
6546
  const { client, calls } = makeTelemetryClient([]); // unprovisioned caller
6200
6547
  await defaultCollectTelemetry(client, false, hqRoot);
6201
- expect(calls.listByType).toBe(1); // orphan-safe probe ran
6548
+ expect(calls.listByType).toBe(2); // person probe then agent probe
6202
6549
  expect(calls.getTelemetryOptIn).toBe(0); // collectors never ran
6203
6550
  expect(calls.postUsage).toBe(0);
6204
6551
  expect(calls.postSkillInvocations).toBe(0);
@@ -6220,6 +6567,79 @@ describe("defaultCollectTelemetry person-entity gate", () => {
6220
6567
  fs.rmSync(hqRoot, { recursive: true, force: true });
6221
6568
  }
6222
6569
  });
6570
+ it("runs telemetry for an agent machine identity with no person entity", async () => {
6571
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-tele-agent-"));
6572
+ try {
6573
+ const AGENT = {
6574
+ uid: "agt_01KUSAGEAGENTTEST00000000",
6575
+ type: "agent",
6576
+ slug: "jeff",
6577
+ status: "active",
6578
+ bucketName: "hq-vault-agt_01KUSAGEAGENTTEST00000000",
6579
+ createdAt: "2026-06-01T00:00:00Z",
6580
+ };
6581
+ const { client, calls } = makeTelemetryClient([], [AGENT]);
6582
+ await defaultCollectTelemetry(client, false, hqRoot, undefined, undefined, AGENT.uid);
6583
+ expect(calls.listByType).toBe(2);
6584
+ // Both collectors forceCollect — neither probes GET /v1/usage/opt-in.
6585
+ expect(calls.getTelemetryOptIn).toBe(0);
6586
+ // Passed the gate: identity resolution writes machine-id before collecting.
6587
+ expect(fs.existsSync(path.join(hqRoot, ".hq", "machine-id"))).toBe(true);
6588
+ }
6589
+ finally {
6590
+ fs.rmSync(hqRoot, { recursive: true, force: true });
6591
+ }
6592
+ });
6593
+ it("does not force-collect when a claimed agent uid is absent from the self-listing", async () => {
6594
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-tele-agent-miss-"));
6595
+ try {
6596
+ const AGENT = {
6597
+ uid: "agt_01KUSAGEAGENTTEST00000000",
6598
+ type: "agent",
6599
+ slug: "jeff",
6600
+ status: "active",
6601
+ bucketName: "hq-vault-agt_01KUSAGEAGENTTEST00000000",
6602
+ createdAt: "2026-06-01T00:00:00Z",
6603
+ };
6604
+ const { client, calls } = makeTelemetryClient([], [AGENT]);
6605
+ await defaultCollectTelemetry(client, false, hqRoot, undefined, undefined, "agt_01KSOMEONEELSE00000000000");
6606
+ expect(calls.listByType).toBe(2);
6607
+ expect(calls.getTelemetryOptIn).toBe(0);
6608
+ expect(fs.existsSync(path.join(hqRoot, ".hq", "machine-id"))).toBe(false);
6609
+ }
6610
+ finally {
6611
+ fs.rmSync(hqRoot, { recursive: true, force: true });
6612
+ }
6613
+ });
6614
+ it("does not treat any agt_ listing as self when more than one agent is visible", async () => {
6615
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-tele-agent-many-"));
6616
+ try {
6617
+ const a = {
6618
+ uid: "agt_01KUSAGEAGENTTEST00000000",
6619
+ type: "agent",
6620
+ slug: "jeff",
6621
+ status: "active",
6622
+ bucketName: "hq-vault-agt_01KUSAGEAGENTTEST00000000",
6623
+ createdAt: "2026-06-01T00:00:00Z",
6624
+ };
6625
+ const b = {
6626
+ uid: "agt_01KUSAGEAGENTOTHER0000000",
6627
+ type: "agent",
6628
+ slug: "cx",
6629
+ status: "active",
6630
+ bucketName: "hq-vault-agt_01KUSAGEAGENTOTHER0000000",
6631
+ createdAt: "2026-06-02T00:00:00Z",
6632
+ };
6633
+ const { client, calls } = makeTelemetryClient([], [a, b]);
6634
+ await defaultCollectTelemetry(client, false, hqRoot);
6635
+ expect(calls.listByType).toBe(2);
6636
+ expect(calls.getTelemetryOptIn).toBe(0);
6637
+ expect(fs.existsSync(path.join(hqRoot, ".hq", "machine-id"))).toBe(false);
6638
+ }
6639
+ finally {
6640
+ fs.rmSync(hqRoot, { recursive: true, force: true });
6641
+ }
6642
+ });
6223
6643
  it("fails open: a thrown person probe still lets telemetry run (no provisioned-user regression)", async () => {
6224
6644
  const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-tele-throw-"));
6225
6645
  try {