@indigoai-us/hq-cloud 6.16.65 → 6.16.66

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 (126) hide show
  1. package/dist/bin/sync-runner-company.d.ts +14 -0
  2. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  3. package/dist/bin/sync-runner-company.js +42 -8
  4. package/dist/bin/sync-runner-company.js.map +1 -1
  5. package/dist/bin/sync-runner-company.test.js +62 -7
  6. package/dist/bin/sync-runner-company.test.js.map +1 -1
  7. package/dist/bin/sync-runner-planning.d.ts +12 -3
  8. package/dist/bin/sync-runner-planning.d.ts.map +1 -1
  9. package/dist/bin/sync-runner-planning.js +65 -3
  10. package/dist/bin/sync-runner-planning.js.map +1 -1
  11. package/dist/bin/sync-runner-planning.test.js +172 -1
  12. package/dist/bin/sync-runner-planning.test.js.map +1 -1
  13. package/dist/bin/sync-runner-watch-loop.d.ts +51 -2
  14. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  15. package/dist/bin/sync-runner-watch-loop.js +409 -165
  16. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  17. package/dist/bin/sync-runner-watch-routes.d.ts +19 -2
  18. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
  19. package/dist/bin/sync-runner-watch-routes.js +40 -5
  20. package/dist/bin/sync-runner-watch-routes.js.map +1 -1
  21. package/dist/bin/sync-runner-watch-routes.test.js +47 -0
  22. package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
  23. package/dist/bin/sync-runner.d.ts +59 -5
  24. package/dist/bin/sync-runner.d.ts.map +1 -1
  25. package/dist/bin/sync-runner.js +48 -4
  26. package/dist/bin/sync-runner.js.map +1 -1
  27. package/dist/bin/sync-runner.test.js +1553 -192
  28. package/dist/bin/sync-runner.test.js.map +1 -1
  29. package/dist/cli/onepass-pull-planner-regressions.test.d.ts +2 -0
  30. package/dist/cli/onepass-pull-planner-regressions.test.d.ts.map +1 -0
  31. package/dist/cli/onepass-pull-planner-regressions.test.js +300 -0
  32. package/dist/cli/onepass-pull-planner-regressions.test.js.map +1 -0
  33. package/dist/cli/onepass-pull-planner.test.d.ts +2 -0
  34. package/dist/cli/onepass-pull-planner.test.d.ts.map +1 -0
  35. package/dist/cli/onepass-pull-planner.test.js +266 -0
  36. package/dist/cli/onepass-pull-planner.test.js.map +1 -0
  37. package/dist/cli/push-scan-exclusions.test.js +4 -4
  38. package/dist/cli/push-scan-exclusions.test.js.map +1 -1
  39. package/dist/cli/share.d.ts +31 -1
  40. package/dist/cli/share.d.ts.map +1 -1
  41. package/dist/cli/share.js +136 -6
  42. package/dist/cli/share.js.map +1 -1
  43. package/dist/cli/share.test.js +295 -5
  44. package/dist/cli/share.test.js.map +1 -1
  45. package/dist/cli/streaming-pull-planner.bench.test.d.ts +15 -0
  46. package/dist/cli/streaming-pull-planner.bench.test.d.ts.map +1 -0
  47. package/dist/cli/streaming-pull-planner.bench.test.js +273 -0
  48. package/dist/cli/streaming-pull-planner.bench.test.js.map +1 -0
  49. package/dist/cli/sync-scope.test.js +8 -0
  50. package/dist/cli/sync-scope.test.js.map +1 -1
  51. package/dist/cli/sync.d.ts +12 -0
  52. package/dist/cli/sync.d.ts.map +1 -1
  53. package/dist/cli/sync.js +297 -125
  54. package/dist/cli/sync.js.map +1 -1
  55. package/dist/cli/sync.test.js +255 -2
  56. package/dist/cli/sync.test.js.map +1 -1
  57. package/dist/journal-row-store.d.ts +40 -5
  58. package/dist/journal-row-store.d.ts.map +1 -1
  59. package/dist/journal-row-store.js +240 -21
  60. package/dist/journal-row-store.js.map +1 -1
  61. package/dist/journal-row-store.test.js +54 -0
  62. package/dist/journal-row-store.test.js.map +1 -1
  63. package/dist/journal-snapshot-v4.d.ts.map +1 -1
  64. package/dist/journal-snapshot-v4.js +11 -6
  65. package/dist/journal-snapshot-v4.js.map +1 -1
  66. package/dist/journal-store.test.js +324 -7
  67. package/dist/journal-store.test.js.map +1 -1
  68. package/dist/journal.d.ts +51 -16
  69. package/dist/journal.d.ts.map +1 -1
  70. package/dist/journal.js +303 -79
  71. package/dist/journal.js.map +1 -1
  72. package/dist/lib/exit-codes.d.ts +15 -0
  73. package/dist/lib/exit-codes.d.ts.map +1 -1
  74. package/dist/lib/exit-codes.js +15 -0
  75. package/dist/lib/exit-codes.js.map +1 -1
  76. package/dist/personal-vault.d.ts.map +1 -1
  77. package/dist/personal-vault.js +10 -3
  78. package/dist/personal-vault.js.map +1 -1
  79. package/dist/personal-vault.test.js +35 -0
  80. package/dist/personal-vault.test.js.map +1 -1
  81. package/dist/remote-pull.d.ts.map +1 -1
  82. package/dist/remote-pull.js +9 -2
  83. package/dist/remote-pull.js.map +1 -1
  84. package/dist/remote-pull.test.js +59 -2
  85. package/dist/remote-pull.test.js.map +1 -1
  86. package/dist/s3.d.ts +1 -0
  87. package/dist/s3.d.ts.map +1 -1
  88. package/dist/s3.js +20 -2
  89. package/dist/s3.js.map +1 -1
  90. package/dist/s3.list-cost.test.d.ts +2 -0
  91. package/dist/s3.list-cost.test.d.ts.map +1 -0
  92. package/dist/s3.list-cost.test.js +51 -0
  93. package/dist/s3.list-cost.test.js.map +1 -0
  94. package/dist/sync/area-ledger.d.ts +27 -3
  95. package/dist/sync/area-ledger.d.ts.map +1 -1
  96. package/dist/sync/area-ledger.js +149 -39
  97. package/dist/sync/area-ledger.js.map +1 -1
  98. package/dist/sync/area-ledger.test.js +192 -5
  99. package/dist/sync/area-ledger.test.js.map +1 -1
  100. package/dist/sync/parallel-sync-coordinator.test.js +29 -0
  101. package/dist/sync/parallel-sync-coordinator.test.js.map +1 -1
  102. package/dist/sync/realtime-telemetry.d.ts +1 -1
  103. package/dist/sync/realtime-telemetry.d.ts.map +1 -1
  104. package/dist/sync/realtime-telemetry.js +14 -6
  105. package/dist/sync/realtime-telemetry.js.map +1 -1
  106. package/dist/sync/realtime-telemetry.test.js +136 -16
  107. package/dist/sync/realtime-telemetry.test.js.map +1 -1
  108. package/dist/sync/remote-file-spool.d.ts +16 -0
  109. package/dist/sync/remote-file-spool.d.ts.map +1 -0
  110. package/dist/sync/remote-file-spool.js +91 -0
  111. package/dist/sync/remote-file-spool.js.map +1 -0
  112. package/dist/sync/remote-file-spool.test.d.ts +2 -0
  113. package/dist/sync/remote-file-spool.test.d.ts.map +1 -0
  114. package/dist/sync/remote-file-spool.test.js +77 -0
  115. package/dist/sync/remote-file-spool.test.js.map +1 -0
  116. package/dist/sync/state-store.d.ts +6 -0
  117. package/dist/sync/state-store.d.ts.map +1 -1
  118. package/dist/sync/state-store.js +14 -2
  119. package/dist/sync/state-store.js.map +1 -1
  120. package/dist/watcher.d.ts +12 -0
  121. package/dist/watcher.d.ts.map +1 -1
  122. package/dist/watcher.js +131 -0
  123. package/dist/watcher.js.map +1 -1
  124. package/dist/watcher.test.js +63 -0
  125. package/dist/watcher.test.js.map +1 -1
  126. package/package.json +2 -1
@@ -10,6 +10,9 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
10
10
  import * as fs from "fs";
11
11
  import * as os from "os";
12
12
  import * as path from "path";
13
+ import * as v8 from "node:v8";
14
+ import * as vm from "node:vm";
15
+ import { monitorEventLoopDelay } from "node:perf_hooks";
13
16
  import { runRunner, runRunnerWithLoop, resolveDeletePolicy, resolveSkipPersonal, resolveSkipCompanies, selectObjectIOFactory, routeChangeToTarget, buildTargetedPushArgv, resolvePullScope, readPinnedPrefixes, SESSIONS_SCOPE_PREFIX, defaultCollectTelemetry, AUTH_REQUIRED_PASS_EXIT, } from "./sync-runner.js";
14
17
  import { SESSION_LOGS_PUSH_ONLY_PREFIX } from "../sync/pull-scope.js";
15
18
  import { AUTH_WAIT_INITIAL_RECHECK_MS, AUTH_WAIT_RESUME_MESSAGE, AUTH_WAIT_START_MESSAGE, AUTH_WAIT_STEADY_RECHECK_MS, } from "./sync-runner-auth-wait.js";
@@ -17,17 +20,20 @@ import { PresignObjectIO, S3SdkObjectIO } from "../object-io.js";
17
20
  import { setBandwidthGovernorForTesting } from "../bandwidth.js";
18
21
  import { POST_SYNC_FLAG_KEYS, } from "./sync-runner-post-sync-flags.js";
19
22
  import { FakeClock, TreeWatcher, } from "../watcher.js";
20
- 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, normalizeCoordinatorPaths, shouldQuarantineWatcherDeleteIntents, shouldUseAreaStorage, WATCHER_REDISCOVERY_RETRY_MS, WATCHER_REDISCOVERY_RETRY_MAX_MS, } from "./sync-runner-watch-loop.js";
21
- import { AreaLedgerMigration, JournalStore, openJournalStateStoreForMigration, PERSONAL_VAULT_JOURNAL_SLUG, readJournal, writeJournal, } from "../journal.js";
23
+ 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, CHORE_STARVATION_CAP_MS, CooperativePassScheduler, normalizeCoordinatorPaths, shouldQuarantineWatcherDeleteIntents, shouldUseAreaStorage, WATCHER_REDISCOVERY_RETRY_MS, WATCHER_REDISCOVERY_RETRY_MAX_MS, } from "./sync-runner-watch-loop.js";
24
+ import { AreaLedgerMigration, JournalStore, openJournalStateStoreForMigration, PERSONAL_VAULT_JOURNAL_SLUG, readJournal, readJournalScoped, writeJournal, } from "../journal.js";
25
+ import { AreaLedger } from "../sync/area-ledger.js";
22
26
  import { AreaResolver } from "../sync/area-resolver.js";
27
+ import { AREA_LEGACY_MIRROR_LINGER_MS } from "../sync/area-ledger-migration.js";
23
28
  import { AreaMaintenanceCursorStore } from "../sync/per-area-maintenance.js";
24
29
  import { HQ_CLOUD_VERSION } from "../version.js";
25
30
  import { lockPathFor, OPERATION_LOCKED_EXIT } from "../operation-lock.js";
26
31
  import { TRANSIENT_NETWORK_EXIT } from "../lib/net-errors.js";
27
32
  import { getStateStoreSnapshotRecoveryCountForTest, resetStateStoreSnapshotRecoveryCountForTest, StateStoreLockError, } from "../sync/state-store.js";
28
- import { PARTIAL_SYNC_EXIT } from "../lib/exit-codes.js";
33
+ import { PARTIAL_SYNC_EXIT, UNRESOLVED_COMPANY_EXIT, } from "../lib/exit-codes.js";
29
34
  import { isCoveredByAny } from "../prefix-coalesce.js";
30
35
  import { VaultAuthError, VaultNotFoundError } from "../vault-client.js";
36
+ import { buildRoutePullArgv } from "./sync-runner-watch-routes.js";
31
37
  import { CognitoAuthError, CognitoRefreshError } from "../cognito-auth.js";
32
38
  import { reconcileCompanyManifest, } from "../manifest-reconcile.js";
33
39
  import yaml from "js-yaml";
@@ -166,6 +172,7 @@ function makeVaultStub(opts = {}) {
166
172
  bucketName: `bucket-${uid}`,
167
173
  status: "active",
168
174
  })),
175
+ findInMyNamespace: opts.findInMyNamespace ?? (() => Promise.resolve(null)),
169
176
  listByType: opts.listPersons ??
170
177
  (() => Promise.resolve([])),
171
178
  },
@@ -958,6 +965,164 @@ describe("target resolution", () => {
958
965
  const call = deps.sync.mock.calls[0][0];
959
966
  expect(call.company).toBe("cmp_abc");
960
967
  });
968
+ it("returns the unresolved-company exit for an empty single-company plan instead of emitting a clean all-complete", async () => {
969
+ const deps = makeDeps({
970
+ createVaultClient: () => makeVaultStub({
971
+ entityGet: () => Promise.resolve(undefined),
972
+ }),
973
+ });
974
+ expect(await runRunner(["--company", "cmp_missing"], deps)).toBe(UNRESOLVED_COMPANY_EXIT);
975
+ expect(deps.stderr.events()).toContainEqual({
976
+ type: "error",
977
+ company: "cmp_missing",
978
+ path: "(company-resolution)",
979
+ message: "company entity lookup returned no entity",
980
+ });
981
+ expect(deps.stdout.events().some((event) => event.type === "all-complete")).toBe(false);
982
+ expect(deps.sync).not.toHaveBeenCalled();
983
+ });
984
+ it("returns the unresolved-company exit when a cmp-prefixed route gets a 404", async () => {
985
+ const client = makeVaultStub({
986
+ entityGet: () => Promise.reject(new VaultNotFoundError("company deleted")),
987
+ });
988
+ const findInMyNamespace = vi.fn().mockResolvedValue(null);
989
+ client.entity.findInMyNamespace = findInMyNamespace;
990
+ const deps = makeDeps({
991
+ createVaultClient: () => client,
992
+ });
993
+ expect(await runRunner(["--company", "cmp_ghost"], deps)).toBe(UNRESOLVED_COMPANY_EXIT);
994
+ expect(findInMyNamespace).toHaveBeenCalledWith("company", "cmp_ghost");
995
+ expect(deps.stderr.events()).toContainEqual({
996
+ type: "error",
997
+ company: "cmp_ghost",
998
+ path: "(company-resolution)",
999
+ message: "company entity lookup returned 404",
1000
+ });
1001
+ expect(deps.stdout.events().some((event) => event.type === "all-complete")).toBe(false);
1002
+ });
1003
+ it("reports an intentional skipCompanies skip without treating it as a failed pass", async () => {
1004
+ const previous = process.env.HQ_SYNC_SKIP_COMPANIES;
1005
+ process.env.HQ_SYNC_SKIP_COMPANIES = "paused";
1006
+ try {
1007
+ const deps = makeDeps({
1008
+ createVaultClient: () => makeVaultStub({
1009
+ entityGet: (uid) => Promise.resolve({
1010
+ uid,
1011
+ slug: "paused",
1012
+ type: "company",
1013
+ status: "active",
1014
+ }),
1015
+ }),
1016
+ });
1017
+ expect(await runRunner(["--company", "cmp_paused"], deps)).toBe(0);
1018
+ expect(deps.stdout.events()).toContainEqual({
1019
+ type: "company-skipped",
1020
+ company: "paused",
1021
+ reason: "skip-companies",
1022
+ });
1023
+ expect(deps.stderr.events().some((event) => event.type === "error")).toBe(false);
1024
+ expect(deps.stdout.events().some((event) => event.type === "all-complete")).toBe(false);
1025
+ expect(deps.sync).not.toHaveBeenCalled();
1026
+ }
1027
+ finally {
1028
+ if (previous === undefined)
1029
+ delete process.env.HQ_SYNC_SKIP_COMPANIES;
1030
+ else
1031
+ process.env.HQ_SYNC_SKIP_COMPANIES = previous;
1032
+ }
1033
+ });
1034
+ it("resolves a slug emitted by the company watch route to its namespace UID", async () => {
1035
+ const company = {
1036
+ uid: "cmp_indigo",
1037
+ slug: "indigo",
1038
+ type: "company",
1039
+ status: "active",
1040
+ bucketName: "bucket-cmp_indigo",
1041
+ };
1042
+ const client = makeVaultStub({
1043
+ entityGet: (uid) => Promise.resolve(uid === company.uid ? company : undefined),
1044
+ });
1045
+ const findInMyNamespace = vi.fn().mockResolvedValue(company);
1046
+ client.entity.findInMyNamespace = findInMyNamespace;
1047
+ const deps = makeDeps({ createVaultClient: () => client });
1048
+ const argv = buildRoutePullArgv({ kind: "company", slug: "indigo" }, ["--companies", "--watch", "--hq-root", "/tmp/hq"]);
1049
+ expect(argv).toEqual([
1050
+ "--company",
1051
+ "indigo",
1052
+ "--direction",
1053
+ "pull",
1054
+ "--hq-root",
1055
+ "/tmp/hq",
1056
+ ]);
1057
+ expect(await runRunner(argv, deps)).toBe(0);
1058
+ expect(findInMyNamespace).toHaveBeenCalledWith("company", "indigo");
1059
+ expect(deps.sync).toHaveBeenCalledWith(expect.objectContaining({ company: "cmp_indigo" }));
1060
+ expect(deps.stdout.events()).toContainEqual({
1061
+ type: "fanout-plan",
1062
+ companies: [
1063
+ expect.objectContaining({ uid: "cmp_indigo", slug: "indigo" }),
1064
+ ],
1065
+ });
1066
+ });
1067
+ it("resolves a cmp-prefixed directory slug in the caller namespace before treating it as a UID", async () => {
1068
+ const company = {
1069
+ uid: "cmp_01JTEAM000000000000000000",
1070
+ slug: "cmp_team",
1071
+ type: "company",
1072
+ status: "active",
1073
+ bucketName: "bucket-cmp-team",
1074
+ };
1075
+ const get = vi.fn().mockImplementation((uid) => Promise.resolve(uid === company.uid ? company : undefined));
1076
+ const client = makeVaultStub({ entityGet: get });
1077
+ const findInMyNamespace = vi.fn().mockResolvedValue(company);
1078
+ client.entity.findInMyNamespace = findInMyNamespace;
1079
+ const deps = makeDeps({ createVaultClient: () => client });
1080
+ const argv = buildRoutePullArgv({ kind: "company", slug: "cmp_team" }, ["--companies", "--watch", "--hq-root", "/tmp/hq"]);
1081
+ expect(await runRunner(argv, deps)).toBe(0);
1082
+ expect(findInMyNamespace).toHaveBeenCalledWith("company", "cmp_team");
1083
+ expect(get).toHaveBeenCalledWith(company.uid);
1084
+ expect(deps.sync).toHaveBeenCalledWith(expect.objectContaining({ company: company.uid }));
1085
+ expect(deps.stdout.events()).toContainEqual({
1086
+ type: "fanout-plan",
1087
+ companies: [expect.objectContaining({ uid: company.uid, slug: "cmp_team" })],
1088
+ });
1089
+ });
1090
+ it("falls back to a literal company UID after caller-namespace slug resolution misses", async () => {
1091
+ const uid = "cmp_01JUID0000000000000000000";
1092
+ const company = {
1093
+ uid,
1094
+ slug: "operator-company",
1095
+ type: "company",
1096
+ status: "active",
1097
+ bucketName: "bucket-operator-company",
1098
+ };
1099
+ const get = vi.fn().mockResolvedValue(company);
1100
+ const client = makeVaultStub({ entityGet: get });
1101
+ const findInMyNamespace = vi.fn().mockResolvedValue(null);
1102
+ client.entity.findInMyNamespace = findInMyNamespace;
1103
+ const deps = makeDeps({ createVaultClient: () => client });
1104
+ expect(await runRunner(["--company", uid], deps)).toBe(0);
1105
+ expect(findInMyNamespace).toHaveBeenCalledWith("company", uid);
1106
+ expect(get).toHaveBeenCalledWith(uid);
1107
+ expect(deps.sync).toHaveBeenCalledWith(expect.objectContaining({ company: uid }));
1108
+ });
1109
+ it("reports a foreign company slug when the literal UID fallback finds nothing", async () => {
1110
+ const get = vi.fn().mockRejectedValue(new VaultNotFoundError("company not found"));
1111
+ const client = makeVaultStub({ entityGet: get });
1112
+ const findInMyNamespace = vi.fn().mockResolvedValue(null);
1113
+ client.entity.findInMyNamespace = findInMyNamespace;
1114
+ const deps = makeDeps({ createVaultClient: () => client });
1115
+ expect(await runRunner(["--company", "indigo"], deps)).toBe(UNRESOLVED_COMPANY_EXIT);
1116
+ expect(findInMyNamespace).toHaveBeenCalledWith("company", "indigo");
1117
+ expect(get).toHaveBeenCalledWith("indigo");
1118
+ expect(deps.sync).not.toHaveBeenCalled();
1119
+ expect(deps.stderr.events()).toContainEqual({
1120
+ type: "error",
1121
+ company: "indigo",
1122
+ path: "(company-resolution)",
1123
+ message: "company entity lookup returned 404",
1124
+ });
1125
+ });
961
1126
  });
962
1127
  // ---------------------------------------------------------------------------
963
1128
  // --personal mode (personal-vault-only, skip company fanout)
@@ -1555,6 +1720,43 @@ describe("automatic journal maintenance", () => {
1555
1720
  },
1556
1721
  ]);
1557
1722
  });
1723
+ it("signals watch readiness after journal preparation and before fanout", async () => {
1724
+ const order = [];
1725
+ let releaseFanout = () => { };
1726
+ const fanout = new Promise((resolve) => { releaseFanout = resolve; });
1727
+ const deps = makeDeps({
1728
+ createVaultClient: () => makeVaultStub({
1729
+ memberships: [{ companyUid: "cmp_indigo" }],
1730
+ entityGet: (uid) => Promise.resolve({ uid, slug: "indigo" }),
1731
+ }),
1732
+ repairJournalStateIfNeeded: vi.fn((slug) => {
1733
+ order.push(`repair:${slug}`);
1734
+ return { status: "not-needed", slug };
1735
+ }),
1736
+ migratePersonalVaultJournal: vi.fn(() => {
1737
+ order.push("seed:personal");
1738
+ }),
1739
+ onJournalsReady: () => {
1740
+ order.push("journals-ready");
1741
+ },
1742
+ sync: vi.fn(async () => {
1743
+ order.push("fanout");
1744
+ await fanout;
1745
+ return defaultSyncResult();
1746
+ }),
1747
+ });
1748
+ const running = runRunner(["--companies", "--skip-personal"], deps);
1749
+ await vi.waitFor(() => expect(order).toContain("fanout"));
1750
+ expect(order).toEqual([
1751
+ "repair:indigo",
1752
+ "repair:personal",
1753
+ "seed:personal",
1754
+ "journals-ready",
1755
+ "fanout",
1756
+ ]);
1757
+ releaseFanout();
1758
+ await expect(running).resolves.toBe(0);
1759
+ });
1558
1760
  it("continues the pass when one journal repair is transiently lock-contended", async () => {
1559
1761
  const repaired = [];
1560
1762
  const sync = vi.fn().mockResolvedValue(defaultSyncResult());
@@ -4178,6 +4380,17 @@ describe("routeChangeToTarget", () => {
4178
4380
  });
4179
4381
  expect(routeChangeToTarget("companies/acme/projects/x/prd.json")).toEqual({ kind: "company", slug: "acme" });
4180
4382
  });
4383
+ it("keeps a company-root directory delete attributable without routing company-root files", () => {
4384
+ // A whole-company removal has no child path, but it still owns every
4385
+ // journal row below the slug and must reach the delete-intent guard.
4386
+ expect(routeChangeToTarget("companies/indigo", "unlinkDir")).toEqual({
4387
+ kind: "company",
4388
+ slug: "indigo",
4389
+ });
4390
+ // A file directly under companies/ is shared metadata, never a tenant.
4391
+ expect(routeChangeToTarget("companies/manifest.yaml", "change")).toBeNull();
4392
+ expect(routeChangeToTarget("companies/.DS_Store", "add")).toBeNull();
4393
+ });
4181
4394
  it("routes non-company top-levels to the personal target", () => {
4182
4395
  expect(routeChangeToTarget("personal/notes.md")).toEqual({
4183
4396
  kind: "personal",
@@ -4202,6 +4415,12 @@ describe("routeChangeToTarget", () => {
4202
4415
  // edit always lands at companies/<slug>/..., never at bare "companies".
4203
4416
  expect(routeChangeToTarget("companies")).toBeNull();
4204
4417
  expect(routeChangeToTarget("companies/")).toBeNull();
4418
+ // The companies directory itself contains shared manifest and scaffold
4419
+ // files. They do not belong to a company journal, so a direct child must
4420
+ // never become a bogus `--company <filename>` pass.
4421
+ expect(routeChangeToTarget("companies/manifest.yaml")).toBeNull();
4422
+ expect(routeChangeToTarget("companies/.DS_Store")).toBeNull();
4423
+ expect(routeChangeToTarget("companies/../personal/notes.md")).toBeNull();
4205
4424
  });
4206
4425
  it("does not route the company scaffold to cloud sync", () => {
4207
4426
  expect(routeChangeToTarget("companies/_template/knowledge/README.md")).toBeNull();
@@ -4283,6 +4502,26 @@ async function flushLoopMicrotasks(n = 8) {
4283
4502
  for (let i = 0; i < n; i++)
4284
4503
  await Promise.resolve();
4285
4504
  }
4505
+ /** Retained journal rows live in typed-array backing stores as well as heap. */
4506
+ function forceGarbageCollectionForTest() {
4507
+ const existing = globalThis.gc;
4508
+ if (typeof existing === "function") {
4509
+ existing();
4510
+ existing();
4511
+ return;
4512
+ }
4513
+ v8.setFlagsFromString("--expose-gc");
4514
+ const collect = vm.runInNewContext("gc");
4515
+ v8.setFlagsFromString("--no-expose-gc");
4516
+ if (typeof collect !== "function")
4517
+ throw new Error("test could not expose GC");
4518
+ collect();
4519
+ collect();
4520
+ }
4521
+ function retainedProcessBytes() {
4522
+ const usage = process.memoryUsage();
4523
+ return usage.heapUsed + usage.external + usage.arrayBuffers;
4524
+ }
4286
4525
  function makeSteppableSleep() {
4287
4526
  const pending = [];
4288
4527
  return {
@@ -4295,8 +4534,19 @@ function makeSteppableSleep() {
4295
4534
  throw new Error("poll sleep was not pending");
4296
4535
  resolve();
4297
4536
  },
4537
+ pendingCount() {
4538
+ return pending.length;
4539
+ },
4298
4540
  };
4299
4541
  }
4542
+ async function waitForPollSleep(sleep) {
4543
+ for (let attempt = 0; attempt < 200; attempt += 1) {
4544
+ if (sleep.pendingCount() > 0)
4545
+ return;
4546
+ await new Promise((resolve) => setImmediate(resolve));
4547
+ }
4548
+ throw new Error("timed out waiting for a poll sleep");
4549
+ }
4300
4550
  // ---------------------------------------------------------------------------
4301
4551
  // Adaptive remote-poll backoff — whole-box CPU aware (no --poll-remote-ms)
4302
4552
  // ---------------------------------------------------------------------------
@@ -5434,6 +5684,83 @@ describe("runRunnerWithLoop — event-push wiring", () => {
5434
5684
  triggerShutdown();
5435
5685
  await loop;
5436
5686
  });
5687
+ it.each([
5688
+ ["serialized", "0"],
5689
+ ["parallel coordinator", "1"],
5690
+ ])("starts event ingress once after journals are ready while the first %s full pass is unfinished", async (_mode, coordinator) => {
5691
+ const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
5692
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = coordinator;
5693
+ const watcher = makeWatcherStub();
5694
+ const createWatcher = vi.fn(() => watcher);
5695
+ let releaseFullPass = () => { };
5696
+ const fullPass = new Promise((resolve) => {
5697
+ releaseFullPass = resolve;
5698
+ });
5699
+ let activeDurableWriters = 0;
5700
+ let maxDurableWriters = 0;
5701
+ const runPass = vi.fn(async (...args) => {
5702
+ const journalsReady = args[2];
5703
+ activeDurableWriters += 1;
5704
+ maxDurableWriters = Math.max(maxDurableWriters, activeDurableWriters);
5705
+ journalsReady?.();
5706
+ await fullPass;
5707
+ activeDurableWriters -= 1;
5708
+ return completedPassOutcome();
5709
+ });
5710
+ let shutdown = () => { };
5711
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
5712
+ runPass: runPass,
5713
+ runPassSupportsCooperativeCheckpoint: true,
5714
+ createWatcher,
5715
+ sleep: () => new Promise(() => { }),
5716
+ onShutdownSignal: (handler) => {
5717
+ shutdown = handler;
5718
+ return () => { };
5719
+ },
5720
+ });
5721
+ try {
5722
+ await flushLoopMicrotasks(30);
5723
+ expect(runPass).toHaveBeenCalledOnce();
5724
+ expect(createWatcher).toHaveBeenCalledOnce();
5725
+ expect(watcher.started).toBe(true);
5726
+ expect(maxDurableWriters).toBe(1);
5727
+ await flushLoopMicrotasks();
5728
+ expect(runPass).toHaveBeenCalledOnce();
5729
+ }
5730
+ finally {
5731
+ releaseFullPass();
5732
+ shutdown();
5733
+ await loop;
5734
+ if (previousCoordinator === undefined)
5735
+ delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
5736
+ else
5737
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
5738
+ }
5739
+ });
5740
+ it.each([
5741
+ ["auth-required", AUTH_REQUIRED_PASS_EXIT],
5742
+ ["journal initialization failure", 1],
5743
+ ])("does not arm event ingress when the first pass exits %s before journal readiness", async (_reason, exitCode) => {
5744
+ const watcher = makeWatcherStub();
5745
+ let shutdown = () => { };
5746
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
5747
+ runPass: vi.fn().mockResolvedValue(exitCode),
5748
+ createWatcher: () => watcher,
5749
+ sleep: () => new Promise(() => { }),
5750
+ onShutdownSignal: (handler) => {
5751
+ shutdown = handler;
5752
+ return () => { };
5753
+ },
5754
+ });
5755
+ try {
5756
+ await flushLoopMicrotasks(30);
5757
+ expect(watcher.started).toBe(false);
5758
+ }
5759
+ finally {
5760
+ shutdown();
5761
+ await expect(loop).resolves.toBe(0);
5762
+ }
5763
+ });
5437
5764
  it("pushes a file created before watcher ready through the warm-up catch-up", async () => {
5438
5765
  const watcher = makeBatchWatcherStub();
5439
5766
  let markReady = () => { };
@@ -5752,7 +6079,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
5752
6079
  });
5753
6080
  await expect(superseded).resolves.toBeUndefined();
5754
6081
  expect(runPass.mock.calls.filter(([argv]) => argv.includes("superseded.md"))).toHaveLength(0);
5755
- expect(stderr.mock.calls.map(([chunk]) => String(chunk)).join("")).toContain('"event":"sync.realtime.timing","schemaVersion":1,"source":"receiver","direction":"pull","outcome":"refused"');
6082
+ expect(stderr.mock.calls.map(([chunk]) => String(chunk)).join("")).toContain('"event":"sync.realtime.timing","schemaVersion":2,"source":"receiver","direction":"pull","outcome":"refused"');
5756
6083
  releaseHeld();
5757
6084
  await heldReceiverWork;
5758
6085
  await replacement;
@@ -6065,7 +6392,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
6065
6392
  triggerShutdown();
6066
6393
  await loop;
6067
6394
  });
6068
- it("fast lane: a small watcher batch dispatches during a slow bulk pass", async () => {
6395
+ it("fast lane: a small watcher batch waits for a full pass that has not reached a hand-off checkpoint", async () => {
6069
6396
  const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-watch-"));
6070
6397
  const relativePath = "companies/indigo/realtime.md";
6071
6398
  const absolutePath = path.join(hqRoot, relativePath);
@@ -6105,15 +6432,17 @@ describe("runRunnerWithLoop — event-push wiring", () => {
6105
6432
  expect(order).toEqual(["bulk"]);
6106
6433
  watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
6107
6434
  await flushLoopMicrotasks(30);
6108
- // Regression: origin/main leaves this as ["bulk"] until releaseBulk().
6109
- expect(order).toEqual(["bulk", "fast", "fast"]);
6435
+ // A full pass that has not reached its durable checkpoint remains the sole
6436
+ // owner. Realtime is promoted at the checkpoint, never run concurrently.
6437
+ expect(order).toEqual(["bulk"]);
6110
6438
  releaseBulk();
6111
- await flushLoopMicrotasks();
6439
+ await flushLoopMicrotasks(30);
6440
+ expect(order).toEqual(["bulk", "fast", "fast"]);
6112
6441
  triggerShutdown();
6113
6442
  await loop;
6114
6443
  fs.rmSync(hqRoot, { recursive: true, force: true });
6115
6444
  });
6116
- it("parallel coordinator: a watcher push starts while a full chore is blocked", async () => {
6445
+ it("parallel coordinator: a watcher push waits for a full chore without a hand-off checkpoint", async () => {
6117
6446
  const previous = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
6118
6447
  process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
6119
6448
  const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-parallel-watch-"));
@@ -6156,9 +6485,10 @@ describe("runRunnerWithLoop — event-push wiring", () => {
6156
6485
  expect(order).toEqual(["chore"]);
6157
6486
  watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
6158
6487
  await flushLoopMicrotasks(30);
6159
- expect(order).toEqual(["chore", "realtime"]);
6488
+ expect(order).toEqual(["chore"]);
6160
6489
  releaseBulk();
6161
- await flushLoopMicrotasks();
6490
+ await flushLoopMicrotasks(30);
6491
+ expect(order).toEqual(["chore", "realtime"]);
6162
6492
  triggerShutdown();
6163
6493
  await loop;
6164
6494
  }
@@ -6243,7 +6573,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
6243
6573
  fs.rmSync(hqRoot, { recursive: true, force: true });
6244
6574
  }
6245
6575
  });
6246
- it("parallel coordinator partitions receiver events into fast and slow realtime lanes", async () => {
6576
+ it("parallel coordinator partitions receiver events into fast and slow realtime lanes without overlapping their durable writes", async () => {
6247
6577
  const previous = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
6248
6578
  process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
6249
6579
  const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-parallel-receiver-lanes-"));
@@ -6314,7 +6644,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
6314
6644
  signal: new AbortController().signal,
6315
6645
  });
6316
6646
  await flushLoopMicrotasks(20);
6317
- expect(order).toEqual(["slow-held", "fast-small"]);
6647
+ expect(order).toEqual(["slow-held"]);
6318
6648
  releaseSlow();
6319
6649
  await held;
6320
6650
  await partitioned;
@@ -6480,96 +6810,594 @@ describe("runRunnerWithLoop — event-push wiring", () => {
6480
6810
  fs.rmSync(hqRoot, { recursive: true, force: true });
6481
6811
  }
6482
6812
  });
6483
- it("fast lane: prefers declared receiver sizes and falls back to the journal", async () => {
6484
- const previous = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
6813
+ it("bounds a 32-path realtime personal commit on a 700k-row journal and 50k-path tree", async () => {
6814
+ const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
6485
6815
  process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
6486
- const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-receiver-declared-size-"));
6816
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-production-scale-watch-"));
6817
+ const watchedRoot = path.join(hqRoot, "personal", "watch-load");
6818
+ const targetPaths = Array.from({ length: 32 }, (_, index) => `personal/watch-load/${String(index).padStart(5, "0")}/target.md`);
6819
+ const files = {};
6820
+ for (let index = 0; index < 700_000; index += 1) {
6821
+ files[`personal/history/${String(index % 50_000).padStart(5, "0")}/entry-${index}.md`] = {
6822
+ hash: `sha256:history-${index}`,
6823
+ size: index % 4096,
6824
+ syncedAt: "2026-09-16T21:30:00.000Z",
6825
+ direction: "up",
6826
+ };
6827
+ }
6828
+ for (const target of targetPaths) {
6829
+ files[target] = {
6830
+ hash: `sha256:${target}`,
6831
+ size: 1,
6832
+ syncedAt: "2026-09-16T21:30:00.000Z",
6833
+ direction: "up",
6834
+ };
6835
+ }
6836
+ writeJournal(PERSONAL_VAULT_JOURNAL_SLUG, {
6837
+ version: "2",
6838
+ lastSync: "2026-09-16T21:30:00.000Z",
6839
+ pulls: [],
6840
+ files,
6841
+ });
6842
+ for (let index = 0; index < 50_000; index += 1) {
6843
+ const directory = path.join(watchedRoot, String(index).padStart(5, "0"));
6844
+ fs.mkdirSync(directory, { recursive: true });
6845
+ fs.writeFileSync(path.join(directory, "seed.md"), "seed");
6846
+ }
6487
6847
  const watcher = makeBatchWatcherStub();
6488
- let receiverSync;
6489
- let triggerShutdown = () => { };
6490
- const event = (relativePath, sizeBytes) => ({
6491
- kind: "upsert",
6492
- relativePath,
6493
- contentHash: "sha256:remote",
6494
- mtime: "2026-08-28T00:00:00.000Z",
6495
- ...(sizeBytes === undefined ? {} : { sizeBytes }),
6496
- originDeviceId: "peer-device",
6497
- originTenantId: "tenant-indigo",
6498
- sequenceNumber: 1,
6499
- eventTimestamp: "2026-08-28T00:00:00.000Z",
6848
+ let shutdown = () => { };
6849
+ let committedAt;
6850
+ const runPass = vi.fn(async (argv) => {
6851
+ if (!argv.includes("--scope-path")) {
6852
+ // Production opens the packed journal during the initial full pass,
6853
+ // before the watcher accepts realtime work. That recovered projection
6854
+ // is the one a scoped commit must reuse.
6855
+ readJournal(PERSONAL_VAULT_JOURNAL_SLUG);
6856
+ }
6857
+ if (argv.includes("--scope-path") && argv.includes("push")) {
6858
+ const scoped = readJournalScoped(PERSONAL_VAULT_JOURNAL_SLUG, targetPaths);
6859
+ const target = targetPaths[0];
6860
+ scoped.files[target] = {
6861
+ ...scoped.files[target],
6862
+ syncedAt: "2026-09-16T21:38:18.000Z",
6863
+ };
6864
+ writeJournal(PERSONAL_VAULT_JOURNAL_SLUG, scoped);
6865
+ committedAt = performance.now();
6866
+ }
6867
+ return completedPassOutcome();
6500
6868
  });
6501
- const journal = readJournal("indigo");
6502
- journal.files["journal-small.md"] = {
6503
- hash: "sha256:journal-small",
6504
- size: 16,
6505
- syncedAt: "2026-08-28T00:00:00.000Z",
6506
- direction: "down",
6507
- };
6508
- writeJournal("indigo", journal);
6509
- const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "pull", "--hq-root", hqRoot], {
6510
- runPass: vi.fn(async () => completedPassOutcome()),
6869
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "push", "--hq-root", hqRoot], {
6870
+ runPass,
6511
6871
  createWatcher: () => watcher,
6512
- createReceiver: ({ syncBatchFn }) => {
6513
- receiverSync = syncBatchFn;
6514
- return { connected: true, start: async () => { }, dispose: async () => { } };
6515
- },
6516
6872
  sleep: () => new Promise(() => { }),
6517
6873
  onShutdownSignal: (handler) => {
6518
- triggerShutdown = handler;
6874
+ shutdown = handler;
6519
6875
  return () => { };
6520
6876
  },
6521
6877
  });
6522
6878
  try {
6523
6879
  await flushLoopMicrotasks(30);
6524
- const batches = receiverSync.partition([
6525
- event("companies/indigo/declared-small.md", 16),
6526
- event("companies/indigo/declared-large.md", DEFAULT_FAST_LANE_MAX_FILE_BYTES + 1),
6527
- event("companies/indigo/journal-small.md"),
6528
- event("companies/indigo/unknown.md"),
6529
- ]);
6530
- expect(batches.map((batch) => batch.map((event) => event.relativePath))).toEqual([
6531
- [
6532
- "companies/indigo/declared-small.md",
6533
- "companies/indigo/journal-small.md",
6534
- ],
6535
- [
6536
- "companies/indigo/declared-large.md",
6537
- "companies/indigo/unknown.md",
6538
- ],
6539
- ]);
6880
+ runPass.mockClear();
6881
+ resetStateStoreSnapshotRecoveryCountForTest();
6882
+ const eventLoopDelay = monitorEventLoopDelay({ resolution: 10 });
6883
+ eventLoopDelay.enable();
6884
+ await new Promise((resolve) => setTimeout(resolve, 0));
6885
+ const submittedAt = performance.now();
6886
+ let timerDelayMs;
6887
+ const eventLoopTurn = new Promise((resolve) => {
6888
+ setTimeout(() => {
6889
+ timerDelayMs = performance.now() - submittedAt;
6890
+ resolve();
6891
+ }, 0);
6892
+ });
6893
+ const batch = {
6894
+ paths: new Map(targetPaths.map((relativePath) => [path.join(hqRoot, relativePath), relativePath])),
6895
+ changes: new Map(targetPaths.map((relativePath) => [
6896
+ path.join(hqRoot, relativePath),
6897
+ { kind: "change" },
6898
+ ])),
6899
+ };
6900
+ watcher.emit(targetPaths[0], batch);
6901
+ await vi.waitFor(() => expect(committedAt).toBeDefined(), { timeout: 30_000, interval: 10 });
6902
+ await eventLoopTurn;
6903
+ eventLoopDelay.disable();
6904
+ expect(committedAt - submittedAt).toBeLessThan(1_000);
6905
+ expect(Number(eventLoopDelay.max) / 1_000_000).toBeLessThan(100);
6906
+ expect(timerDelayMs).toBeLessThan(100);
6907
+ expect(getStateStoreSnapshotRecoveryCountForTest()).toBe(0);
6908
+ expect(runPass.mock.calls.filter(([argv]) => argv.includes("--scope-path"))).toHaveLength(1);
6540
6909
  }
6541
6910
  finally {
6542
- triggerShutdown();
6911
+ shutdown();
6543
6912
  await loop;
6544
- if (previous === undefined)
6913
+ if (previousCoordinator === undefined)
6545
6914
  delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
6546
6915
  else
6547
- process.env.HQ_SYNC_PARALLEL_COORDINATOR = previous;
6916
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
6548
6917
  fs.rmSync(hqRoot, { recursive: true, force: true });
6549
6918
  }
6550
- });
6551
- it("fast lane: does not charge declared sizes from a skipped personal route", async () => {
6919
+ }, 180_000);
6920
+ it("keeps an area-authoritative 700k-row personal aggregate hot across alternating realtime and company batches", async () => {
6552
6921
  const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
6553
- const previousFastFiles = process.env[FAST_LANE_MAX_FILES_ENV];
6554
6922
  process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
6555
- process.env[FAST_LANE_MAX_FILES_ENV] = "1";
6556
- const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-receiver-skipped-route-size-"));
6557
- const watcher = makeBatchWatcherStub();
6558
- let receiverSync;
6559
- let triggerShutdown = () => { };
6560
- const event = (relativePath) => ({
6561
- kind: "upsert",
6562
- relativePath,
6563
- contentHash: "sha256:remote",
6564
- mtime: "2026-08-28T00:00:00.000Z",
6565
- sizeBytes: 64,
6566
- originDeviceId: "peer-device",
6567
- originTenantId: "tenant-indigo",
6568
- sequenceNumber: 1,
6569
- eventTimestamp: "2026-08-28T00:00:00.000Z",
6570
- });
6571
- const loop = runRunnerWithLoop([
6572
- "--companies", "--skip-personal", "--watch", "--event-push", "--direction", "pull",
6923
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-multi-journal-watch-"));
6924
+ const watchedRoot = path.join(hqRoot, "personal", "watch-load");
6925
+ const personalPath = "personal/watch-load/00000/target.md";
6926
+ const companyPath = "companies/indigo/watch-load/00000/target.md";
6927
+ const companyKey = "watch-load/00000/target.md";
6928
+ const personalFiles = {};
6929
+ for (let index = 0; index < 700_000; index += 1) {
6930
+ personalFiles[`personal/history-${index % 5}/${String(index % 50_000).padStart(5, "0")}/entry-${index}.md`] = {
6931
+ hash: `sha256:history-${index}`,
6932
+ size: index % 4096,
6933
+ syncedAt: "2026-09-16T21:30:00.000Z",
6934
+ direction: "up",
6935
+ };
6936
+ }
6937
+ personalFiles[personalPath] = {
6938
+ hash: "sha256:personal-target",
6939
+ size: 1,
6940
+ syncedAt: "2026-09-16T21:30:00.000Z",
6941
+ direction: "up",
6942
+ };
6943
+ const resolver = new AreaResolver({
6944
+ format: 1,
6945
+ routerVersion: 1,
6946
+ areas: [
6947
+ ...Array.from({ length: 5 }, (_, index) => ({
6948
+ id: `root:history-${index}`,
6949
+ prefixes: [`personal/history-${index}`],
6950
+ paths: [],
6951
+ })),
6952
+ { id: "root:watch", prefixes: ["personal/watch-load"], paths: [] },
6953
+ { id: "root:@files", prefixes: [], paths: [], rootFiles: true },
6954
+ ],
6955
+ });
6956
+ writeJournal(PERSONAL_VAULT_JOURNAL_SLUG, {
6957
+ version: "2",
6958
+ lastSync: "2026-09-16T21:30:00.000Z",
6959
+ pulls: [],
6960
+ files: {},
6961
+ });
6962
+ writeJournal("indigo", {
6963
+ version: "2",
6964
+ lastSync: "2026-09-16T21:30:00.000Z",
6965
+ pulls: [],
6966
+ files: {
6967
+ [companyKey]: {
6968
+ hash: "sha256:company-target",
6969
+ size: 1,
6970
+ syncedAt: "2026-09-16T21:30:00.000Z",
6971
+ direction: "up",
6972
+ },
6973
+ },
6974
+ });
6975
+ const migration = new AreaLedgerMigration({
6976
+ rootDir: __runnerTestStateDir,
6977
+ journalSlug: PERSONAL_VAULT_JOURNAL_SLUG,
6978
+ resolver,
6979
+ legacyStore: openJournalStateStoreForMigration(PERSONAL_VAULT_JOURNAL_SLUG),
6980
+ });
6981
+ expect(migration.cutOver(120_000).phase).toBe("area-authoritative");
6982
+ migration.dispose();
6983
+ // The migration state itself is covered by the area-ledger migration
6984
+ // suite. Seed the already-authoritative production shape in one durable
6985
+ // transaction so this watch-loop test measures repeated realtime reads,
6986
+ // rather than spending most of its worker budget replaying a one-time
6987
+ // legacy-to-area copy.
6988
+ openJournalStateStoreForMigration(PERSONAL_VAULT_JOURNAL_SLUG).replaceState({
6989
+ journal: {
6990
+ version: "2",
6991
+ lastSync: "2026-09-16T21:30:00.000Z",
6992
+ pulls: [],
6993
+ files: personalFiles,
6994
+ },
6995
+ });
6996
+ AreaLedger.open({
6997
+ rootDir: __runnerTestStateDir,
6998
+ journalSlug: PERSONAL_VAULT_JOURNAL_SLUG,
6999
+ layoutEpoch: 1,
7000
+ areaIds: resolver.layout.areas.map((area) => area.id),
7001
+ resolveArea: (key) => resolver.resolve(key),
7002
+ }).append({
7003
+ upserts: personalFiles,
7004
+ metadata: { lastSync: "2026-09-16T21:30:00.000Z", pulls: [] },
7005
+ });
7006
+ fs.mkdirSync(watchedRoot, { recursive: true });
7007
+ for (let index = 0; index < 50_000; index += 1) {
7008
+ fs.writeFileSync(path.join(watchedRoot, `seed-${String(index).padStart(5, "0")}.md`), "seed");
7009
+ }
7010
+ fs.mkdirSync(path.dirname(path.join(hqRoot, personalPath)), { recursive: true });
7011
+ fs.writeFileSync(path.join(hqRoot, personalPath), "personal");
7012
+ fs.mkdirSync(path.dirname(path.join(hqRoot, companyPath)), { recursive: true });
7013
+ fs.writeFileSync(path.join(hqRoot, companyPath), "company");
7014
+ const watcher = makeBatchWatcherStub();
7015
+ const sleep = makeSteppableSleep();
7016
+ let shutdown = () => { };
7017
+ let isUntrackedDelete = (_relativePath) => false;
7018
+ const commits = [];
7019
+ let submittedAt = 0;
7020
+ const aggregatePrime = vi.spyOn(AreaLedger.prototype, "primeAggregateCache");
7021
+ const runPass = vi.fn(async (argv) => {
7022
+ if (!argv.includes("--scope-path")) {
7023
+ readJournal(PERSONAL_VAULT_JOURNAL_SLUG);
7024
+ readJournal("indigo");
7025
+ }
7026
+ if (argv.includes("--scope-path") && argv.includes("push")) {
7027
+ const slug = argv.includes("--company") ? "indigo" : PERSONAL_VAULT_JOURNAL_SLUG;
7028
+ const key = slug === "indigo" ? companyKey : personalPath;
7029
+ const scoped = readJournalScoped(slug, [key]);
7030
+ scoped.files[key] = {
7031
+ ...scoped.files[key],
7032
+ syncedAt: "2026-09-16T21:38:18.000Z",
7033
+ };
7034
+ writeJournal(slug, scoped);
7035
+ commits.push({ slug, elapsedMs: performance.now() - submittedAt });
7036
+ }
7037
+ return completedPassOutcome();
7038
+ });
7039
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "push", "--hq-root", hqRoot], {
7040
+ runPass,
7041
+ createWatcher: (options) => {
7042
+ isUntrackedDelete = options.isUntrackedDelete;
7043
+ return watcher;
7044
+ },
7045
+ startEventSync: vi.fn().mockResolvedValue(null),
7046
+ sleep: sleep.sleep,
7047
+ onShutdownSignal: (handler) => {
7048
+ shutdown = handler;
7049
+ return () => { };
7050
+ },
7051
+ });
7052
+ const emitAndWait = async (relativePaths) => {
7053
+ const before = commits.length;
7054
+ if (relativePaths.includes(personalPath)) {
7055
+ // This is the same non-blocking watcher callback which starts an
7056
+ // openJournalStoreSessionAsync before a delete episode is drained.
7057
+ // It is intentionally issued before each realtime batch: a completed
7058
+ // watch pass must not have thrown away the warmed area aggregate.
7059
+ expect(isUntrackedDelete(personalPath)).toBe(false);
7060
+ await new Promise((resolve) => setImmediate(resolve));
7061
+ }
7062
+ submittedAt = performance.now();
7063
+ const batch = {
7064
+ paths: new Map(relativePaths.map((relativePath) => [path.join(hqRoot, relativePath), relativePath])),
7065
+ changes: new Map(relativePaths.map((relativePath) => [
7066
+ path.join(hqRoot, relativePath),
7067
+ { kind: "change" },
7068
+ ])),
7069
+ };
7070
+ watcher.emit(relativePaths[0], batch);
7071
+ await vi.waitFor(() => expect(commits.length).toBe(before + relativePaths.length), {
7072
+ timeout: 30_000,
7073
+ interval: 10,
7074
+ });
7075
+ // `commits` is observed inside the injected pass, before the runner has
7076
+ // completed its pass-final cleanup. Let that cleanup run before the next
7077
+ // watcher episode so the assertion observes the cache lifetime at the
7078
+ // same boundary as a real completed watch pass.
7079
+ await flushLoopMicrotasks(30);
7080
+ };
7081
+ try {
7082
+ await flushLoopMicrotasks(30);
7083
+ runPass.mockClear();
7084
+ // The initial runner pass is the one permitted cold open. Every later
7085
+ // scoped batch must reuse that authoritative aggregate. Warm the same
7086
+ // yielding watcher-session path that production uses before its first
7087
+ // delete episode, then measure only post-warmup primes.
7088
+ aggregatePrime.mockClear();
7089
+ expect(isUntrackedDelete(personalPath)).toBe(false);
7090
+ await new Promise((resolve) => setImmediate(resolve));
7091
+ // The initial full pass may already have opened this bridge (zero new
7092
+ // primes), or this watcher callback may perform the one permitted cold
7093
+ // warm-up. The post-poll assertion below is the regression boundary.
7094
+ expect(aggregatePrime.mock.calls.length).toBeLessThanOrEqual(1);
7095
+ aggregatePrime.mockClear();
7096
+ // Reproduce the normal poll boundary observed on the outposts. On the
7097
+ // current implementation it drops every cached area bridge even though
7098
+ // the watcher and runner remain alive.
7099
+ sleep.tick();
7100
+ await flushLoopMicrotasks(30);
7101
+ forceGarbageCollectionForTest();
7102
+ const retainedAfterWarmupBytes = retainedProcessBytes();
7103
+ const postWarmCpuStart = process.cpuUsage();
7104
+ const postWarmStartedAt = performance.now();
7105
+ resetStateStoreSnapshotRecoveryCountForTest();
7106
+ const eventLoopDelay = monitorEventLoopDelay({ resolution: 10 });
7107
+ eventLoopDelay.enable();
7108
+ await emitAndWait([personalPath, companyPath]);
7109
+ for (let round = 0; round < 3; round += 1) {
7110
+ await emitAndWait([personalPath]);
7111
+ await emitAndWait([companyPath]);
7112
+ }
7113
+ eventLoopDelay.disable();
7114
+ const recoveryCount = getStateStoreSnapshotRecoveryCountForTest();
7115
+ const maxDelayMs = Number(eventLoopDelay.max) / 1_000_000;
7116
+ const maxCommitMs = Math.max(...commits.map((commit) => commit.elapsedMs));
7117
+ // A first area delta briefly retains the reverse-shadow mirror to
7118
+ // amortize a burst. Retained memory means after that documented linger
7119
+ // has released its second full-vault projection, not during it.
7120
+ await new Promise((resolve) => setTimeout(resolve, AREA_LEGACY_MIRROR_LINGER_MS + 25));
7121
+ forceGarbageCollectionForTest();
7122
+ const retainedAfterChurnBytes = retainedProcessBytes();
7123
+ const retainedGrowthBytes = retainedAfterChurnBytes - retainedAfterWarmupBytes;
7124
+ const postWarmCpu = process.cpuUsage(postWarmCpuStart);
7125
+ const postWarmCpuPercent = ((postWarmCpu.user + postWarmCpu.system) /
7126
+ ((performance.now() - postWarmStartedAt) * 1_000)) * 100;
7127
+ console.info({
7128
+ aggregatePrimeCount: aggregatePrime.mock.calls.length,
7129
+ recoveryCount,
7130
+ maxDelayMs,
7131
+ maxCommitMs,
7132
+ postWarmCpuPercent,
7133
+ retainedAfterWarmupBytes,
7134
+ retainedAfterChurnBytes,
7135
+ retainedGrowthBytes,
7136
+ commits: commits.length,
7137
+ });
7138
+ expect(aggregatePrime).not.toHaveBeenCalled();
7139
+ // The first area delta opens its reverse-shadow v3 mirror once; its
7140
+ // linger keeps all later scoped commits off snapshot recovery. This is
7141
+ // deliberately exact so a second cold bridge cannot hide as a cache hit.
7142
+ expect.soft(recoveryCount).toBe(1);
7143
+ // The production-shape run remains bounded under the coverage job's
7144
+ // concurrent workers: CI measured 1,090.52 ms of timer delay and
7145
+ // 1,087.79 ms to commit. 1.5 s retains a concrete latency ceiling while
7146
+ // leaving headroom for that instrumentation, rather than making the
7147
+ // assertion depend on a single host's scheduling jitter.
7148
+ expect.soft(maxDelayMs).toBeLessThan(1_500);
7149
+ expect.soft(maxCommitMs).toBeLessThan(1_500);
7150
+ expect.soft(retainedGrowthBytes).toBeLessThan(32 * 1024 * 1024);
7151
+ expect(commits.map((commit) => commit.slug)).toEqual([
7152
+ PERSONAL_VAULT_JOURNAL_SLUG,
7153
+ "indigo",
7154
+ PERSONAL_VAULT_JOURNAL_SLUG,
7155
+ "indigo",
7156
+ PERSONAL_VAULT_JOURNAL_SLUG,
7157
+ "indigo",
7158
+ PERSONAL_VAULT_JOURNAL_SLUG,
7159
+ "indigo",
7160
+ ]);
7161
+ }
7162
+ finally {
7163
+ aggregatePrime.mockRestore();
7164
+ shutdown();
7165
+ await loop;
7166
+ if (previousCoordinator === undefined)
7167
+ delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
7168
+ else
7169
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
7170
+ fs.rmSync(hqRoot, { recursive: true, force: true });
7171
+ }
7172
+ }, 180_000);
7173
+ it("keeps warmed personal and company journals hot across alternating realtime batches", async () => {
7174
+ const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
7175
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
7176
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-multi-journal-legacy-watch-"));
7177
+ const watchedRoot = path.join(hqRoot, "personal", "watch-load");
7178
+ const personalPath = "personal/watch-load/00000/target.md";
7179
+ const companyPath = "companies/indigo/watch-load/00000/target.md";
7180
+ const companyKey = "watch-load/00000/target.md";
7181
+ const personalFiles = {};
7182
+ for (let index = 0; index < 700_000; index += 1) {
7183
+ personalFiles[`personal/history/${String(index % 50_000).padStart(5, "0")}/entry-${index}.md`] = {
7184
+ hash: `sha256:history-${index}`,
7185
+ size: index % 4096,
7186
+ syncedAt: "2026-09-16T21:30:00.000Z",
7187
+ direction: "up",
7188
+ };
7189
+ }
7190
+ personalFiles[personalPath] = {
7191
+ hash: "sha256:personal-target",
7192
+ size: 1,
7193
+ syncedAt: "2026-09-16T21:30:00.000Z",
7194
+ direction: "up",
7195
+ };
7196
+ writeJournal(PERSONAL_VAULT_JOURNAL_SLUG, {
7197
+ version: "2",
7198
+ lastSync: "2026-09-16T21:30:00.000Z",
7199
+ pulls: [],
7200
+ files: personalFiles,
7201
+ });
7202
+ writeJournal("indigo", {
7203
+ version: "2",
7204
+ lastSync: "2026-09-16T21:30:00.000Z",
7205
+ pulls: [],
7206
+ files: {
7207
+ [companyKey]: {
7208
+ hash: "sha256:company-target",
7209
+ size: 1,
7210
+ syncedAt: "2026-09-16T21:30:00.000Z",
7211
+ direction: "up",
7212
+ },
7213
+ },
7214
+ });
7215
+ for (let index = 0; index < 50_000; index += 1) {
7216
+ const directory = path.join(watchedRoot, String(index).padStart(5, "0"));
7217
+ fs.mkdirSync(directory, { recursive: true });
7218
+ fs.writeFileSync(path.join(directory, "seed.md"), "seed");
7219
+ }
7220
+ fs.mkdirSync(path.dirname(path.join(hqRoot, personalPath)), { recursive: true });
7221
+ fs.writeFileSync(path.join(hqRoot, personalPath), "personal");
7222
+ fs.mkdirSync(path.dirname(path.join(hqRoot, companyPath)), { recursive: true });
7223
+ fs.writeFileSync(path.join(hqRoot, companyPath), "company");
7224
+ const watcher = makeBatchWatcherStub();
7225
+ let shutdown = () => { };
7226
+ const commits = [];
7227
+ let submittedAt = 0;
7228
+ const runPass = vi.fn(async (argv) => {
7229
+ if (!argv.includes("--scope-path")) {
7230
+ readJournal(PERSONAL_VAULT_JOURNAL_SLUG);
7231
+ readJournal("indigo");
7232
+ }
7233
+ if (argv.includes("--scope-path") && argv.includes("push")) {
7234
+ const slug = argv.includes("--company") ? "indigo" : PERSONAL_VAULT_JOURNAL_SLUG;
7235
+ const key = slug === "indigo" ? companyKey : personalPath;
7236
+ const scoped = readJournalScoped(slug, [key]);
7237
+ scoped.files[key] = {
7238
+ ...scoped.files[key],
7239
+ syncedAt: "2026-09-16T21:38:18.000Z",
7240
+ };
7241
+ writeJournal(slug, scoped);
7242
+ commits.push({ slug, elapsedMs: performance.now() - submittedAt });
7243
+ }
7244
+ return completedPassOutcome();
7245
+ });
7246
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "push", "--hq-root", hqRoot], {
7247
+ runPass,
7248
+ createWatcher: () => watcher,
7249
+ sleep: () => new Promise(() => { }),
7250
+ onShutdownSignal: (handler) => {
7251
+ shutdown = handler;
7252
+ return () => { };
7253
+ },
7254
+ });
7255
+ const emitAndWait = async (relativePaths) => {
7256
+ const before = commits.length;
7257
+ submittedAt = performance.now();
7258
+ const batch = {
7259
+ paths: new Map(relativePaths.map((relativePath) => [path.join(hqRoot, relativePath), relativePath])),
7260
+ changes: new Map(relativePaths.map((relativePath) => [
7261
+ path.join(hqRoot, relativePath),
7262
+ { kind: "change" },
7263
+ ])),
7264
+ };
7265
+ watcher.emit(relativePaths[0], batch);
7266
+ await vi.waitFor(() => expect(commits.length).toBe(before + relativePaths.length), {
7267
+ timeout: 30_000,
7268
+ interval: 10,
7269
+ });
7270
+ };
7271
+ try {
7272
+ await flushLoopMicrotasks(30);
7273
+ runPass.mockClear();
7274
+ resetStateStoreSnapshotRecoveryCountForTest();
7275
+ const eventLoopDelay = monitorEventLoopDelay({ resolution: 10 });
7276
+ eventLoopDelay.enable();
7277
+ await emitAndWait([personalPath, companyPath]);
7278
+ for (let round = 0; round < 3; round += 1) {
7279
+ await emitAndWait([personalPath]);
7280
+ await emitAndWait([companyPath]);
7281
+ }
7282
+ eventLoopDelay.disable();
7283
+ const recoveryCount = getStateStoreSnapshotRecoveryCountForTest();
7284
+ const maxDelayMs = Number(eventLoopDelay.max) / 1_000_000;
7285
+ const maxCommitMs = Math.max(...commits.map((commit) => commit.elapsedMs));
7286
+ console.info({ recoveryCount, maxDelayMs, maxCommitMs, commits: commits.length });
7287
+ expect.soft(recoveryCount).toBe(0);
7288
+ expect.soft(maxDelayMs).toBeLessThan(100);
7289
+ expect.soft(maxCommitMs).toBeLessThan(1_000);
7290
+ expect(commits.map((commit) => commit.slug)).toEqual([
7291
+ PERSONAL_VAULT_JOURNAL_SLUG,
7292
+ "indigo",
7293
+ PERSONAL_VAULT_JOURNAL_SLUG,
7294
+ "indigo",
7295
+ PERSONAL_VAULT_JOURNAL_SLUG,
7296
+ "indigo",
7297
+ PERSONAL_VAULT_JOURNAL_SLUG,
7298
+ "indigo",
7299
+ ]);
7300
+ }
7301
+ finally {
7302
+ shutdown();
7303
+ await loop;
7304
+ if (previousCoordinator === undefined)
7305
+ delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
7306
+ else
7307
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
7308
+ fs.rmSync(hqRoot, { recursive: true, force: true });
7309
+ }
7310
+ }, 180_000);
7311
+ it("fast lane: prefers declared receiver sizes and falls back to the journal", async () => {
7312
+ const previous = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
7313
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
7314
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-receiver-declared-size-"));
7315
+ const watcher = makeBatchWatcherStub();
7316
+ let receiverSync;
7317
+ let triggerShutdown = () => { };
7318
+ const event = (relativePath, sizeBytes) => ({
7319
+ kind: "upsert",
7320
+ relativePath,
7321
+ contentHash: "sha256:remote",
7322
+ mtime: "2026-08-28T00:00:00.000Z",
7323
+ ...(sizeBytes === undefined ? {} : { sizeBytes }),
7324
+ originDeviceId: "peer-device",
7325
+ originTenantId: "tenant-indigo",
7326
+ sequenceNumber: 1,
7327
+ eventTimestamp: "2026-08-28T00:00:00.000Z",
7328
+ });
7329
+ const journal = readJournal("indigo");
7330
+ journal.files["journal-small.md"] = {
7331
+ hash: "sha256:journal-small",
7332
+ size: 16,
7333
+ syncedAt: "2026-08-28T00:00:00.000Z",
7334
+ direction: "down",
7335
+ };
7336
+ writeJournal("indigo", journal);
7337
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "pull", "--hq-root", hqRoot], {
7338
+ runPass: vi.fn(async () => completedPassOutcome()),
7339
+ createWatcher: () => watcher,
7340
+ createReceiver: ({ syncBatchFn }) => {
7341
+ receiverSync = syncBatchFn;
7342
+ return { connected: true, start: async () => { }, dispose: async () => { } };
7343
+ },
7344
+ sleep: () => new Promise(() => { }),
7345
+ onShutdownSignal: (handler) => {
7346
+ triggerShutdown = handler;
7347
+ return () => { };
7348
+ },
7349
+ });
7350
+ try {
7351
+ await flushLoopMicrotasks(30);
7352
+ const batches = receiverSync.partition([
7353
+ event("companies/indigo/declared-small.md", 16),
7354
+ event("companies/indigo/declared-large.md", DEFAULT_FAST_LANE_MAX_FILE_BYTES + 1),
7355
+ event("companies/indigo/journal-small.md"),
7356
+ event("companies/indigo/unknown.md"),
7357
+ ]);
7358
+ expect(batches.map((batch) => batch.map((event) => event.relativePath))).toEqual([
7359
+ [
7360
+ "companies/indigo/declared-small.md",
7361
+ "companies/indigo/journal-small.md",
7362
+ ],
7363
+ [
7364
+ "companies/indigo/declared-large.md",
7365
+ "companies/indigo/unknown.md",
7366
+ ],
7367
+ ]);
7368
+ }
7369
+ finally {
7370
+ triggerShutdown();
7371
+ await loop;
7372
+ if (previous === undefined)
7373
+ delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
7374
+ else
7375
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = previous;
7376
+ fs.rmSync(hqRoot, { recursive: true, force: true });
7377
+ }
7378
+ });
7379
+ it("fast lane: does not charge declared sizes from a skipped personal route", async () => {
7380
+ const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
7381
+ const previousFastFiles = process.env[FAST_LANE_MAX_FILES_ENV];
7382
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
7383
+ process.env[FAST_LANE_MAX_FILES_ENV] = "1";
7384
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-receiver-skipped-route-size-"));
7385
+ const watcher = makeBatchWatcherStub();
7386
+ let receiverSync;
7387
+ let triggerShutdown = () => { };
7388
+ const event = (relativePath) => ({
7389
+ kind: "upsert",
7390
+ relativePath,
7391
+ contentHash: "sha256:remote",
7392
+ mtime: "2026-08-28T00:00:00.000Z",
7393
+ sizeBytes: 64,
7394
+ originDeviceId: "peer-device",
7395
+ originTenantId: "tenant-indigo",
7396
+ sequenceNumber: 1,
7397
+ eventTimestamp: "2026-08-28T00:00:00.000Z",
7398
+ });
7399
+ const loop = runRunnerWithLoop([
7400
+ "--companies", "--skip-personal", "--watch", "--event-push", "--direction", "pull",
6573
7401
  "--hq-root", hqRoot,
6574
7402
  ], {
6575
7403
  runPass: vi.fn(async () => completedPassOutcome()),
@@ -6655,19 +7483,19 @@ describe("runRunnerWithLoop — event-push wiring", () => {
6655
7483
  ]),
6656
7484
  });
6657
7485
  await flushLoopMicrotasks(30);
6658
- // origin/main failure: expected a scoped tiny path while the bulk pass is
6659
- // held, but #408 kept all four paths in the slow lane.
6660
- expect(calls.filter((argv) => argv.includes("--scope-path"))).toHaveLength(1);
6661
- expect(calls.flat().join(" ")).toContain("realtime.md");
7486
+ // The full pass owns durable state until a checkpoint. Its queued realtime
7487
+ // residue is promoted at that hand-off instead of overlapping the pass.
7488
+ expect(calls.filter((argv) => argv.includes("--scope-path"))).toHaveLength(0);
6662
7489
  releaseBulk();
6663
7490
  await flushLoopMicrotasks(30);
6664
7491
  expect(calls.filter((argv) => argv.includes("--scope-path"))).toHaveLength(2);
7492
+ expect(calls.flat().join(" ")).toContain("realtime.md");
6665
7493
  expect(calls.flat().join(" ")).toContain("session-0.log");
6666
7494
  triggerShutdown();
6667
7495
  await loop;
6668
7496
  fs.rmSync(hqRoot, { recursive: true, force: true });
6669
7497
  });
6670
- it("fast lane: a small receiver pull dispatches during a slow bulk pass", async () => {
7498
+ it("fast lane: a small receiver pull waits for a slow full pass without a hand-off checkpoint", async () => {
6671
7499
  const watcher = makeWatcherStub();
6672
7500
  let triggerShutdown = () => { };
6673
7501
  let receiverSync;
@@ -6712,7 +7540,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
6712
7540
  watcher.emit();
6713
7541
  await flushLoopMicrotasks();
6714
7542
  expect(order).toEqual(["bulk"]);
6715
- await receiverSync({
7543
+ const receiverPull = receiverSync({
6716
7544
  events: [{
6717
7545
  kind: "upsert", relativePath: "companies/acme/realtime.md", contentHash: "sha256:x",
6718
7546
  mtime: "2026-08-26T00:00:00.000Z", originDeviceId: "peer", originTenantId: "acme",
@@ -6720,8 +7548,11 @@ describe("runRunnerWithLoop — event-push wiring", () => {
6720
7548
  }],
6721
7549
  signal: new AbortController().signal,
6722
7550
  });
6723
- expect(order).toEqual(["bulk", "fast-pull"]);
7551
+ await flushLoopMicrotasks(20);
7552
+ expect(order).toEqual(["bulk"]);
6724
7553
  releaseBulk();
7554
+ await receiverPull;
7555
+ expect(order).toEqual(["bulk", "fast-pull"]);
6725
7556
  await flushLoopMicrotasks();
6726
7557
  triggerShutdown();
6727
7558
  await loop;
@@ -6765,12 +7596,14 @@ describe("runRunnerWithLoop — event-push wiring", () => {
6765
7596
  await flushLoopMicrotasks();
6766
7597
  watcher.emit();
6767
7598
  await flushLoopMicrotasks();
7599
+ releaseBulk();
7600
+ // The full chore has no explicit safe checkpoint in this stub, so the
7601
+ // realtime pass begins only after the chore has released ownership.
6768
7602
  watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
6769
7603
  await fastStarted;
6770
- releaseBulk();
6771
7604
  await flushLoopMicrotasks(20);
6772
- // The slow pass has finished, but its fast bypass still mutates. A second
6773
- // sync/rescue must not acquire the root operation lock in this interval.
7605
+ // The realtime pass owns the root lock until its own durable work ends. A
7606
+ // second sync/rescue must not acquire the lock in this interval.
6774
7607
  expect(fs.existsSync(lockPathFor(hqRoot))).toBe(true);
6775
7608
  releaseFast();
6776
7609
  await flushLoopMicrotasks(20);
@@ -6945,59 +7778,68 @@ describe("runRunnerWithLoop — event-push wiring", () => {
6945
7778
  return () => { };
6946
7779
  },
6947
7780
  });
6948
- await flushLoopMicrotasks();
6949
- watcher.emit();
6950
- await flushLoopMicrotasks();
6951
- runPass.mockClear();
6952
- const events = [
6953
- ...Array.from({ length: 17 }, (_, index) => ({
6954
- kind: "upsert",
6955
- relativePath: `companies/acme/a-${index}.md`,
6956
- contentHash: "sha256:x",
6957
- mtime: "2026-08-26T00:00:00.000Z",
6958
- originDeviceId: "peer",
6959
- originTenantId: "acme",
6960
- sequenceNumber: index,
6961
- eventTimestamp: "2026-08-26T00:00:00.000Z",
6962
- })),
6963
- ...Array.from({ length: 16 }, (_, index) => ({
6964
- kind: "upsert",
6965
- relativePath: `companies/beta/b-${index}.md`,
6966
- contentHash: "sha256:x",
6967
- mtime: "2026-08-26T00:00:00.000Z",
6968
- originDeviceId: "peer",
6969
- originTenantId: "beta",
6970
- sequenceNumber: index + 17,
6971
- eventTimestamp: "2026-08-26T00:00:00.000Z",
6972
- })),
6973
- ];
6974
- for (const [slug, prefix, count] of [["acme", "a", 17], ["beta", "b", 16]]) {
6975
- const journal = readJournal(slug);
6976
- for (let index = 0; index < count; index++) {
6977
- journal.files[`${prefix}-${index}.md`] = {
6978
- hash: "sha256:x",
6979
- size: 16,
6980
- syncedAt: "2026-08-26T00:00:00.000Z",
6981
- direction: "down",
6982
- };
7781
+ try {
7782
+ await flushLoopMicrotasks();
7783
+ watcher.emit();
7784
+ await flushLoopMicrotasks();
7785
+ runPass.mockClear();
7786
+ const events = [
7787
+ ...Array.from({ length: 17 }, (_, index) => ({
7788
+ kind: "upsert",
7789
+ relativePath: `companies/acme/a-${index}.md`,
7790
+ contentHash: "sha256:x",
7791
+ mtime: "2026-08-26T00:00:00.000Z",
7792
+ originDeviceId: "peer",
7793
+ originTenantId: "acme",
7794
+ sequenceNumber: index,
7795
+ eventTimestamp: "2026-08-26T00:00:00.000Z",
7796
+ })),
7797
+ ...Array.from({ length: 16 }, (_, index) => ({
7798
+ kind: "upsert",
7799
+ relativePath: `companies/beta/b-${index}.md`,
7800
+ contentHash: "sha256:x",
7801
+ mtime: "2026-08-26T00:00:00.000Z",
7802
+ originDeviceId: "peer",
7803
+ originTenantId: "beta",
7804
+ sequenceNumber: index + 17,
7805
+ eventTimestamp: "2026-08-26T00:00:00.000Z",
7806
+ })),
7807
+ ];
7808
+ for (const [slug, prefix, count] of [["acme", "a", 17], ["beta", "b", 16]]) {
7809
+ const journal = readJournal(slug);
7810
+ for (let index = 0; index < count; index++) {
7811
+ journal.files[`${prefix}-${index}.md`] = {
7812
+ hash: "sha256:x",
7813
+ size: 16,
7814
+ syncedAt: "2026-08-26T00:00:00.000Z",
7815
+ direction: "down",
7816
+ };
7817
+ }
7818
+ writeJournal(slug, journal);
6983
7819
  }
6984
- writeJournal(slug, journal);
7820
+ const receiverDrain = receiverSync({
7821
+ events,
7822
+ signal: new AbortController().signal,
7823
+ });
7824
+ await flushLoopMicrotasks(20);
7825
+ // The running full chore has not yet yielded a durable hand-off, so no
7826
+ // receiver work may overlap it. Once it releases ownership, the
7827
+ // scheduler admits each route's bounded work before the residual slow
7828
+ // beta batch (acme fast, beta fast, then beta slow).
7829
+ expect(runPass).not.toHaveBeenCalled();
7830
+ releaseBulk();
7831
+ await receiverDrain;
7832
+ const scopedCalls = runPass.mock.calls.filter(([argv]) => argv.includes("--scope-path"));
7833
+ expect(scopedCalls).toHaveLength(3);
7834
+ expect(scopedCalls.flat().join(" ")).toContain("a-16.md");
7835
+ expect(scopedCalls.flat().join(" ")).toContain("b-14.md");
7836
+ }
7837
+ finally {
7838
+ releaseBulk();
7839
+ triggerShutdown();
7840
+ await loop;
7841
+ fs.rmSync(hqRoot, { recursive: true, force: true });
6985
7842
  }
6986
- const receiverDrain = receiverSync({
6987
- events,
6988
- signal: new AbortController().signal,
6989
- });
6990
- await flushLoopMicrotasks(20);
6991
- // The 33-event mixed delivery peels its first 32 bounded paths into the
6992
- // fast lane (17 acme + 15 beta); the final beta event remains slow.
6993
- expect(runPass).toHaveBeenCalledTimes(2);
6994
- expect(runPass.mock.calls.flat().join(" ")).toContain("a-16.md");
6995
- expect(runPass.mock.calls.flat().join(" ")).toContain("b-14.md");
6996
- releaseBulk();
6997
- await receiverDrain;
6998
- triggerShutdown();
6999
- await loop;
7000
- fs.rmSync(hqRoot, { recursive: true, force: true });
7001
7843
  });
7002
7844
  it("fast lane: queues slow work behind a receiver pass that owns the root lock", async () => {
7003
7845
  const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-slow-queue-"));
@@ -7169,9 +8011,12 @@ describe("runRunnerWithLoop — event-push wiring", () => {
7169
8011
  await flushLoopMicrotasks();
7170
8012
  watcher.emit(paths[0][1], { paths: new Map(paths) });
7171
8013
  await flushLoopMicrotasks(30);
7172
- expect(Object.keys(readJournal("indigo").files)).toHaveLength(paths.length);
8014
+ // The full pass has not reached a durable checkpoint, so it remains the
8015
+ // only journal writer. The scoped realtime pass is admitted immediately
8016
+ // after this safe hand-off, preserving both sets of rows.
8017
+ expect(Object.keys(readJournal("indigo").files)).toHaveLength(0);
7173
8018
  releaseBulk();
7174
- await flushLoopMicrotasks();
8019
+ await flushLoopMicrotasks(30);
7175
8020
  const journal = readJournal("indigo").files;
7176
8021
  expect(Object.keys(journal)).toHaveLength(paths.length * 2);
7177
8022
  for (let index = 0; index < paths.length; index++) {
@@ -7223,10 +8068,12 @@ describe("runRunnerWithLoop — event-push wiring", () => {
7223
8068
  watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
7224
8069
  }
7225
8070
  await flushLoopMicrotasks(60);
7226
- expect(completed).toEqual(["fast", "fast", "fast", "fast", "fast"]);
8071
+ // A full pass without a checkpoint remains the sole mutable-state owner.
8072
+ // The queued realtime batches run immediately after it completes.
8073
+ expect(completed).toEqual([]);
7227
8074
  releaseBulk();
7228
- await flushLoopMicrotasks();
7229
- expect(completed).toContain("bulk");
8075
+ await flushLoopMicrotasks(60);
8076
+ expect(completed).toEqual(["bulk", "fast", "fast", "fast", "fast", "fast"]);
7230
8077
  triggerShutdown();
7231
8078
  await loop;
7232
8079
  fs.rmSync(hqRoot, { recursive: true, force: true });
@@ -7460,18 +8307,33 @@ describe("runRunnerWithLoop — event-push wiring", () => {
7460
8307
  // No pending FakeClock timers left behind by the driver's debounce.
7461
8308
  expect(clock.pendingTimerCount()).toBe(0);
7462
8309
  });
7463
- it("a hard-error (exit 1) poll pass returns immediately", async () => {
8310
+ it("a non-auth pass error keeps the watch loop alive for the next poll", async () => {
7464
8311
  const watcher = makeWatcherStub();
7465
8312
  const runPass = vi.fn().mockResolvedValue(1);
7466
- const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
8313
+ const sleep = makeSteppableSleep();
8314
+ let triggerShutdown = () => { };
8315
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
7467
8316
  runPass,
7468
8317
  clock: new FakeClock(),
7469
8318
  createWatcher: () => watcher,
7470
- sleep: () => Promise.resolve(),
7471
- onShutdownSignal: () => () => { },
8319
+ sleep: sleep.sleep,
8320
+ onShutdownSignal: (handler) => {
8321
+ triggerShutdown = handler;
8322
+ return () => { };
8323
+ },
7472
8324
  });
7473
- expect(code).toBe(1);
7474
- expect(watcher.disposed).toBe(false);
8325
+ try {
8326
+ await flushLoopMicrotasks();
8327
+ expect(runPass).toHaveBeenCalledTimes(1);
8328
+ sleep.tick();
8329
+ await flushLoopMicrotasks();
8330
+ expect(runPass).toHaveBeenCalledTimes(2);
8331
+ }
8332
+ finally {
8333
+ triggerShutdown();
8334
+ await expect(loop).resolves.toBe(0);
8335
+ expect(watcher.started).toBe(false);
8336
+ }
7475
8337
  });
7476
8338
  it("an auth-required watch pass parks in-process instead of exiting", async () => {
7477
8339
  const watcher = makeWatcherStub();
@@ -7520,29 +8382,92 @@ describe("runRunnerWithLoop — event-push wiring", () => {
7520
8382
  it("a TRANSIENT_NETWORK_EXIT poll pass does NOT surface as a crash — the loop retries (HQ-SYNC-1W)", async () => {
7521
8383
  // A transient offline blip must keep the watcher alive: the loop logs and
7522
8384
  // polls again instead of returning the code and letting the process exit
7523
- // (which the menubar reports as "watcher exited unexpectedly"). A genuine
7524
- // hard error on a later pass still terminates the loop.
8385
+ // (which the menubar reports as "watcher exited unexpectedly"). A later
8386
+ // generic failure is also retried rather than becoming a daemon exit.
7525
8387
  const watcher = makeWatcherStub();
7526
8388
  let n = 0;
7527
8389
  const runPass = vi.fn().mockImplementation(async () => {
7528
8390
  n++;
7529
- // First pass: machine is offline (transient). Second pass: a real hard
7530
- // error, used here only to end the otherwise-infinite loop deterministically.
8391
+ // First pass: machine is offline (transient). Second pass: a generic
8392
+ // failed pass, which must take the same non-fatal retry path.
7531
8393
  return n === 1 ? TRANSIENT_NETWORK_EXIT : 1;
7532
8394
  });
7533
- const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
8395
+ const sleep = makeSteppableSleep();
8396
+ let triggerShutdown = () => { };
8397
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
7534
8398
  runPass,
7535
8399
  clock: new FakeClock(),
7536
8400
  createWatcher: () => watcher,
7537
- sleep: () => Promise.resolve(),
7538
- onShutdownSignal: () => () => { },
8401
+ sleep: sleep.sleep,
8402
+ onShutdownSignal: (handler) => {
8403
+ triggerShutdown = handler;
8404
+ return () => { };
8405
+ },
7539
8406
  });
7540
- // The transient pass did NOT end the loop — it ran again…
8407
+ await flushLoopMicrotasks();
8408
+ sleep.tick();
8409
+ await flushLoopMicrotasks();
8410
+ // The transient pass did NOT end the loop — it ran again, and neither
8411
+ // error causes the daemon itself to exit.
7541
8412
  expect(runPass).toHaveBeenCalledTimes(2);
7542
- // …and the transient code was never surfaced as the watcher's exit code.
7543
- expect(code).toBe(1);
7544
- expect(code).not.toBe(TRANSIENT_NETWORK_EXIT);
8413
+ triggerShutdown();
8414
+ await expect(loop).resolves.toBe(0);
8415
+ expect(watcher.disposed).toBe(false);
8416
+ });
8417
+ it("keeps the watcher alive for an unresolved company watch route (HQ-SYNC-COMPANY-ROUTE-UID)", async () => {
8418
+ const watcher = makeWatcherStub();
8419
+ const client = makeVaultStub({
8420
+ entityGet: () => Promise.reject(new VaultNotFoundError("company deleted")),
8421
+ });
8422
+ const findInMyNamespace = vi.fn().mockResolvedValue(null);
8423
+ client.entity.findInMyNamespace = findInMyNamespace;
8424
+ const routedPass = makeDeps({ createVaultClient: () => client });
8425
+ const routedArgv = buildRoutePullArgv({ kind: "company", slug: "cmp_ghost" }, ["--companies", "--watch", "--hq-root", "/tmp/hq"]);
8426
+ let attempts = 0;
8427
+ let routedExit;
8428
+ const runPass = vi.fn().mockImplementation(async () => {
8429
+ attempts += 1;
8430
+ if (attempts === 1) {
8431
+ routedExit = await runRunner(routedArgv, routedPass);
8432
+ return routedExit;
8433
+ }
8434
+ return UNRESOLVED_COMPANY_EXIT;
8435
+ });
8436
+ const sleep = makeSteppableSleep();
8437
+ let triggerShutdown = () => { };
8438
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
8439
+ runPass,
8440
+ clock: new FakeClock(),
8441
+ createWatcher: () => watcher,
8442
+ sleep: sleep.sleep,
8443
+ onShutdownSignal: (handler) => {
8444
+ triggerShutdown = handler;
8445
+ return () => { };
8446
+ },
8447
+ });
8448
+ await flushLoopMicrotasks();
8449
+ sleep.tick();
8450
+ await flushLoopMicrotasks();
8451
+ expect(routedArgv).toEqual([
8452
+ "--company",
8453
+ "cmp_ghost",
8454
+ "--direction",
8455
+ "pull",
8456
+ "--hq-root",
8457
+ "/tmp/hq",
8458
+ ]);
8459
+ expect(routedExit).toBe(UNRESOLVED_COMPANY_EXIT);
8460
+ expect(runPass).toHaveBeenCalledTimes(2);
8461
+ triggerShutdown();
8462
+ await expect(loop).resolves.toBe(0);
7545
8463
  expect(watcher.disposed).toBe(false);
8464
+ expect(findInMyNamespace).toHaveBeenCalledWith("company", "cmp_ghost");
8465
+ expect(routedPass.stderr.events()).toContainEqual({
8466
+ type: "error",
8467
+ company: "cmp_ghost",
8468
+ path: "(company-resolution)",
8469
+ message: "company entity lookup returned 404",
8470
+ });
7546
8471
  });
7547
8472
  it("keeps the watch loop alive when a company leg returns transient exit 75 (HQ-SYNC-X)", async () => {
7548
8473
  const watcher = makeWatcherStub();
@@ -7559,24 +8484,36 @@ describe("runRunnerWithLoop — event-push wiring", () => {
7559
8484
  let passes = 0;
7560
8485
  const runPass = vi.fn().mockImplementation(async () => {
7561
8486
  passes += 1;
7562
- // End the otherwise-open watch loop with a later hard failure. The first
7563
- // pass uses the real company-leg fanout path, not a synthetic exit code.
8487
+ // The first pass uses the real company-leg fanout path, not a synthetic
8488
+ // exit code. A later generic failure must also leave the daemon alive.
7564
8489
  return passes === 1 ? runRunner(["--companies"], transientPass) : 1;
7565
8490
  });
7566
- const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
8491
+ const sleep = makeSteppableSleep();
8492
+ let triggerShutdown = () => { };
8493
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
7567
8494
  runPass,
7568
8495
  clock: new FakeClock(),
7569
8496
  createWatcher: () => watcher,
7570
- sleep: () => Promise.resolve(),
7571
- onShutdownSignal: () => () => { },
8497
+ sleep: sleep.sleep,
8498
+ onShutdownSignal: (handler) => {
8499
+ triggerShutdown = handler;
8500
+ return () => { };
8501
+ },
7572
8502
  });
7573
- expect(runPass).toHaveBeenCalledTimes(2);
7574
- expect(code).toBe(1);
7575
- expect(code).not.toBe(TRANSIENT_NETWORK_EXIT);
7576
- expect(transientPass.stderr
7577
- .events()
7578
- .some((event) => event.type === "error" && event.path === "(company)")).toBe(false);
7579
- expect(watcher.disposed).toBe(false);
8503
+ try {
8504
+ await waitForPollSleep(sleep);
8505
+ sleep.tick();
8506
+ await flushLoopMicrotasks();
8507
+ expect(runPass).toHaveBeenCalledTimes(2);
8508
+ expect(transientPass.stderr
8509
+ .events()
8510
+ .some((event) => event.type === "error" && event.path === "(company)")).toBe(false);
8511
+ expect(watcher.disposed).toBe(false);
8512
+ }
8513
+ finally {
8514
+ triggerShutdown();
8515
+ await expect(loop).resolves.toBe(0);
8516
+ }
7580
8517
  });
7581
8518
  it("a PARTIAL_SYNC_EXIT poll pass does NOT surface as a crash — the loop retries", async () => {
7582
8519
  // A partial pass (some per-file transfers threw: oversized object on an
@@ -7590,22 +8527,32 @@ describe("runRunnerWithLoop — event-push wiring", () => {
7590
8527
  let n = 0;
7591
8528
  const runPass = vi.fn().mockImplementation(async () => {
7592
8529
  n++;
7593
- // Two partial passes prove a DETERMINISTIC per-file failure doesn't kill
7594
- // the loop either. A hard error then ends the otherwise-infinite loop.
8530
+ // Two partial passes prove a deterministic per-file failure doesn't kill
8531
+ // the loop either. A generic failure must also leave it alive.
7595
8532
  return n <= 2 ? PARTIAL_SYNC_EXIT : 1;
7596
8533
  });
7597
- const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
8534
+ const sleep = makeSteppableSleep();
8535
+ let triggerShutdown = () => { };
8536
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
7598
8537
  runPass,
7599
8538
  clock: new FakeClock(),
7600
8539
  createWatcher: () => watcher,
7601
- sleep: () => Promise.resolve(),
7602
- onShutdownSignal: () => () => { },
8540
+ sleep: sleep.sleep,
8541
+ onShutdownSignal: (handler) => {
8542
+ triggerShutdown = handler;
8543
+ return () => { };
8544
+ },
7603
8545
  });
8546
+ await flushLoopMicrotasks();
8547
+ sleep.tick();
8548
+ await flushLoopMicrotasks();
8549
+ sleep.tick();
8550
+ await flushLoopMicrotasks();
7604
8551
  // Both partial passes stayed alive — the loop ran a third time…
7605
8552
  expect(runPass).toHaveBeenCalledTimes(3);
7606
- // …and the partial code was never surfaced as the watcher's exit code.
7607
- expect(code).toBe(1);
7608
- expect(code).not.toBe(PARTIAL_SYNC_EXIT);
8553
+ // …and neither partial nor generic pass failure exits the daemon.
8554
+ triggerShutdown();
8555
+ await expect(loop).resolves.toBe(0);
7609
8556
  expect(watcher.disposed).toBe(false);
7610
8557
  });
7611
8558
  it("alerts when a partial pass failed before a company's download leg", async () => {
@@ -7633,24 +8580,33 @@ describe("runRunnerWithLoop — event-push wiring", () => {
7633
8580
  return 1;
7634
8581
  });
7635
8582
  // Every clock read advances a full cadence interval, so the tick after
7636
- // the partial pass is a scheduled full reconcile that returns the exit
7637
- // code this test asserts on.
8583
+ // the partial pass is a scheduled full reconcile with a generic failure.
7638
8584
  let monotonic = 0;
8585
+ const sleep = makeSteppableSleep();
8586
+ let triggerShutdown = () => { };
7639
8587
  try {
7640
- const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
8588
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
7641
8589
  runPass,
7642
8590
  clock: new FakeClock(),
7643
8591
  createWatcher: () => watcher,
7644
8592
  monotonicNow: () => (monotonic += DEFAULT_FULL_RECONCILE_MS),
7645
- sleep: () => Promise.resolve(),
7646
- onShutdownSignal: () => () => { },
8593
+ sleep: sleep.sleep,
8594
+ onShutdownSignal: (handler) => {
8595
+ triggerShutdown = handler;
8596
+ return () => { };
8597
+ },
7647
8598
  });
7648
- expect(code).toBe(1);
8599
+ await flushLoopMicrotasks();
8600
+ sleep.tick();
8601
+ await flushLoopMicrotasks();
8602
+ expect(runPass).toHaveBeenCalledTimes(2);
7649
8603
  const stderr = stderrWrite.mock.calls
7650
8604
  .map(([chunk]) => String(chunk))
7651
8605
  .join("");
7652
8606
  expect(stderr).toContain("hq-sync-runner: COMPANY PASS FAILED company=personal message=RangeError Maximum call stack size exceeded (downloads for this company did not run)");
7653
8607
  expect(stderr).toContain("hq-sync-runner: watch pass completed with per-file errors; failed-companies=personal; staying alive, retrying next poll");
8608
+ triggerShutdown();
8609
+ await expect(loop).resolves.toBe(0);
7654
8610
  }
7655
8611
  finally {
7656
8612
  stderrWrite.mockRestore();
@@ -7837,6 +8793,85 @@ describe("runRunnerWithLoop — watch-mode auth-wait", () => {
7837
8793
  });
7838
8794
  });
7839
8795
  describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
8796
+ it("hands a wake realtime drain to the guard before a long-running full chore completes", async () => {
8797
+ const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
8798
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
8799
+ const watcher = makeWatcherStub();
8800
+ let triggerShutdown = () => { };
8801
+ let releaseCheckpoint = () => { };
8802
+ const checkpointGate = new Promise((resolve) => { releaseCheckpoint = resolve; });
8803
+ let releaseRealtime = () => { };
8804
+ const realtimeGate = new Promise((resolve) => { releaseRealtime = resolve; });
8805
+ const order = [];
8806
+ let passes = 0;
8807
+ let runRealtime = null;
8808
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
8809
+ runPass: vi.fn().mockImplementation(async (_argv, checkpoint) => {
8810
+ passes += 1;
8811
+ if (passes === 2) {
8812
+ order.push("chore");
8813
+ // The fake pass has completed its current durable unit and is now
8814
+ // at the same explicit hand-off seam production uses between
8815
+ // fully-settled fanout targets.
8816
+ await checkpointGate;
8817
+ await checkpoint?.();
8818
+ order.push("chore-complete");
8819
+ }
8820
+ return completedPassOutcome();
8821
+ }),
8822
+ runPassSupportsCooperativeCheckpoint: true,
8823
+ clock: new FakeClock(),
8824
+ createWatcher: () => watcher,
8825
+ sleep: makeSteppableSleep().sleep,
8826
+ onShutdownSignal: (handler) => {
8827
+ triggerShutdown = handler;
8828
+ return () => { };
8829
+ },
8830
+ startRealtimeScheduler: async (opts) => {
8831
+ // This is the same wake path used by the realtime scheduler: it
8832
+ // enters runGuardedTask behind the coordinator worker.
8833
+ runRealtime = () => opts.runGuarded(async () => {
8834
+ order.push("realtime");
8835
+ await realtimeGate;
8836
+ order.push("realtime-complete");
8837
+ });
8838
+ return {
8839
+ signal: () => undefined,
8840
+ checkHighWater: async () => undefined,
8841
+ dispose: async () => undefined,
8842
+ };
8843
+ },
8844
+ });
8845
+ try {
8846
+ await flushLoopMicrotasks(30);
8847
+ watcher.emit();
8848
+ await flushLoopMicrotasks(20);
8849
+ expect(order).toEqual(["chore"]);
8850
+ const realtime = runRealtime();
8851
+ releaseCheckpoint();
8852
+ await flushLoopMicrotasks(30);
8853
+ expect(order).toEqual(["chore", "realtime"]);
8854
+ releaseRealtime();
8855
+ await realtime;
8856
+ await flushLoopMicrotasks(20);
8857
+ expect(order).toEqual([
8858
+ "chore",
8859
+ "realtime",
8860
+ "realtime-complete",
8861
+ "chore-complete",
8862
+ ]);
8863
+ }
8864
+ finally {
8865
+ releaseCheckpoint();
8866
+ releaseRealtime();
8867
+ triggerShutdown();
8868
+ await loop;
8869
+ if (previousCoordinator === undefined)
8870
+ delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
8871
+ else
8872
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
8873
+ }
8874
+ });
7840
8875
  it("feeds a watcher-originated V2 drain through the same single-flight guard as the poll", async () => {
7841
8876
  const watcher = makeWatcherStub();
7842
8877
  let triggerShutdown = () => { };
@@ -7949,6 +8984,173 @@ describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
7949
8984
  await loop;
7950
8985
  });
7951
8986
  });
8987
+ describe("CooperativePassScheduler", () => {
8988
+ it("admits an explicitly safe fast realtime pass while a slow scoped realtime pass remains in flight", async () => {
8989
+ const scheduler = new CooperativePassScheduler(() => 0);
8990
+ const order = [];
8991
+ let releaseSlow = () => { };
8992
+ const slowGate = new Promise((resolve) => { releaseSlow = resolve; });
8993
+ let markSlowStarted = () => { };
8994
+ const slowStarted = new Promise((resolve) => { markSlowStarted = resolve; });
8995
+ const slow = scheduler.submit(async () => {
8996
+ order.push("slow-start");
8997
+ markSlowStarted();
8998
+ await slowGate;
8999
+ order.push("slow-complete");
9000
+ return 0;
9001
+ }, "realtime", "slow");
9002
+ await slowStarted;
9003
+ const fast = scheduler.submit(async () => {
9004
+ order.push("fast");
9005
+ return 0;
9006
+ }, "realtime", "fast", true);
9007
+ try {
9008
+ await Promise.resolve();
9009
+ await Promise.resolve();
9010
+ expect([...order]).toEqual(["slow-start", "fast"]);
9011
+ }
9012
+ finally {
9013
+ releaseSlow();
9014
+ await Promise.all([slow, fast]);
9015
+ }
9016
+ });
9017
+ it("preserves journal order and resumes full work across repeated randomized hand-offs", async () => {
9018
+ // These deterministic seeds deliberately vary the number and enqueue
9019
+ // ordering of realtime tasks. Every append asserts one durable-state owner
9020
+ // at a time; the full continuation must resume after every realtime burst.
9021
+ for (let seed = 1; seed <= 32; seed += 1) {
9022
+ let now = 0;
9023
+ const scheduler = new CooperativePassScheduler(() => now);
9024
+ const journal = [];
9025
+ const expected = new Set();
9026
+ let writer = null;
9027
+ let maximumWriters = 0;
9028
+ const append = async (entry) => {
9029
+ expect(writer).toBeNull();
9030
+ writer = entry;
9031
+ maximumWriters = Math.max(maximumWriters, 1);
9032
+ // A journal append crosses an async durable-write boundary. If the
9033
+ // scheduler admitted another pass here, its ownership assertion would
9034
+ // fail rather than merely observing JavaScript's synchronous turn.
9035
+ await Promise.resolve();
9036
+ expect(writer).toBe(entry);
9037
+ journal.push(entry);
9038
+ writer = null;
9039
+ };
9040
+ let releaseCheckpoint = () => { };
9041
+ const checkpointReady = new Promise((resolve) => { releaseCheckpoint = resolve; });
9042
+ const chore = scheduler.submit(async ({ checkpoint }) => {
9043
+ expected.add(`chore-${seed}-before`);
9044
+ await append(`chore-${seed}-before`);
9045
+ await checkpointReady;
9046
+ await checkpoint();
9047
+ expected.add(`chore-${seed}-after`);
9048
+ await append(`chore-${seed}-after`);
9049
+ return 0;
9050
+ }, "chore");
9051
+ await Promise.resolve();
9052
+ const realtimeCount = (seed % 7) + 1;
9053
+ const shuffledIndexes = Array.from({ length: realtimeCount }, (_, index) => index);
9054
+ let randomState = seed;
9055
+ for (let index = shuffledIndexes.length - 1; index > 0; index -= 1) {
9056
+ randomState = (randomState * 1_664_525 + 1_013_904_223) >>> 0;
9057
+ const swapIndex = randomState % (index + 1);
9058
+ [shuffledIndexes[index], shuffledIndexes[swapIndex]] = [
9059
+ shuffledIndexes[swapIndex],
9060
+ shuffledIndexes[index],
9061
+ ];
9062
+ }
9063
+ const realtime = shuffledIndexes.map((index) => {
9064
+ const entry = `realtime-${seed}-${(index * 17 + seed) % realtimeCount}`;
9065
+ expected.add(entry);
9066
+ return scheduler.submit(async () => {
9067
+ // Shuffle the microtask timing without ever allowing a second writer.
9068
+ if ((seed + index) % 3 === 0)
9069
+ await Promise.resolve();
9070
+ await append(entry);
9071
+ now += 1;
9072
+ return 0;
9073
+ }, "realtime");
9074
+ });
9075
+ releaseCheckpoint();
9076
+ await Promise.all([chore, ...realtime]);
9077
+ expect(maximumWriters).toBe(1);
9078
+ expect(new Set(journal)).toEqual(expected);
9079
+ expect(journal).toHaveLength(expected.size);
9080
+ expect(journal[0]).toBe(`chore-${seed}-before`);
9081
+ expect(journal.at(-1)).toBe(`chore-${seed}-after`);
9082
+ }
9083
+ });
9084
+ it("lets realtime win at 15 minutes minus one millisecond, then gives the chore the exact-boundary admission", async () => {
9085
+ const runCase = async (elapsedMs) => {
9086
+ let now = 0;
9087
+ const scheduler = new CooperativePassScheduler(() => now);
9088
+ const order = [];
9089
+ let releaseActive = () => { };
9090
+ const activeGate = new Promise((resolve) => { releaseActive = resolve; });
9091
+ const activeRealtime = scheduler.submit(async () => {
9092
+ order.push("active-realtime");
9093
+ await activeGate;
9094
+ return 0;
9095
+ }, "realtime");
9096
+ await Promise.resolve();
9097
+ const chore = scheduler.submit(async () => {
9098
+ order.push("chore");
9099
+ return 0;
9100
+ }, "chore");
9101
+ const queuedRealtime = scheduler.submit(async () => {
9102
+ order.push("queued-realtime");
9103
+ return 0;
9104
+ }, "realtime");
9105
+ now = elapsedMs;
9106
+ releaseActive();
9107
+ await Promise.all([activeRealtime, chore, queuedRealtime]);
9108
+ return order;
9109
+ };
9110
+ await expect(runCase(CHORE_STARVATION_CAP_MS - 1)).resolves.toEqual([
9111
+ "active-realtime",
9112
+ "queued-realtime",
9113
+ "chore",
9114
+ ]);
9115
+ await expect(runCase(CHORE_STARVATION_CAP_MS)).resolves.toEqual([
9116
+ "active-realtime",
9117
+ "chore",
9118
+ "queued-realtime",
9119
+ ]);
9120
+ });
9121
+ it("resumes a handed-off chore when a continuous realtime burst reaches the 15-minute cap", async () => {
9122
+ let now = 0;
9123
+ const scheduler = new CooperativePassScheduler(() => now);
9124
+ const order = [];
9125
+ let releaseCheckpoint = () => { };
9126
+ const checkpointReady = new Promise((resolve) => { releaseCheckpoint = resolve; });
9127
+ const chore = scheduler.submit(async ({ checkpoint }) => {
9128
+ order.push("chore-before");
9129
+ await checkpointReady;
9130
+ await checkpoint();
9131
+ order.push("chore-after");
9132
+ return 0;
9133
+ }, "chore");
9134
+ await Promise.resolve();
9135
+ const firstRealtime = scheduler.submit(async () => {
9136
+ order.push("realtime-before-cap");
9137
+ now = CHORE_STARVATION_CAP_MS;
9138
+ return 0;
9139
+ }, "realtime");
9140
+ const laterRealtime = scheduler.submit(async () => {
9141
+ order.push("realtime-after-cap");
9142
+ return 0;
9143
+ }, "realtime");
9144
+ releaseCheckpoint();
9145
+ await Promise.all([chore, firstRealtime, laterRealtime]);
9146
+ expect(order).toEqual([
9147
+ "chore-before",
9148
+ "realtime-before-cap",
9149
+ "chore-after",
9150
+ "realtime-after-cap",
9151
+ ]);
9152
+ });
9153
+ });
7952
9154
  describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
7953
9155
  const watchArgv = [
7954
9156
  "--companies",
@@ -10113,6 +11315,9 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
10113
11315
  "/tmp/hq",
10114
11316
  ], {
10115
11317
  runPass: runPass,
11318
+ ...(opts.supportsCooperativeCheckpoint
11319
+ ? { runPassSupportsCooperativeCheckpoint: true }
11320
+ : {}),
10116
11321
  clock,
10117
11322
  createWatcher: (options) => {
10118
11323
  opts.onCreateWatcher?.({
@@ -10225,6 +11430,123 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
10225
11430
  shutdown();
10226
11431
  await loop;
10227
11432
  });
11433
+ it("records initial-pull writes before its cooperative checkpoint can admit watcher work", async () => {
11434
+ const markContentPublished = vi.fn();
11435
+ const startEventSync = vi.fn().mockResolvedValue({
11436
+ publishBatch: vi.fn(),
11437
+ markContentPublished,
11438
+ receiver: { start: async () => { }, dispose: async () => { }, connected: true },
11439
+ ownDeviceId: "dev-test",
11440
+ dispose: vi.fn().mockResolvedValue(undefined),
11441
+ });
11442
+ const watcher = makeBatchWatcherStub();
11443
+ let suppressionWasInstalledWhenRealtimeStarted;
11444
+ const runPass = vi.fn(async (argv, checkpoint, onJournalsReady, onAppliedUpserts) => {
11445
+ if (argv.includes("--scope-path")) {
11446
+ suppressionWasInstalledWhenRealtimeStarted = markContentPublished
11447
+ .mock.calls
11448
+ .some(([relativePath]) => relativePath === "companies/indigo/initial-pull.md");
11449
+ return completedPassOutcome();
11450
+ }
11451
+ onJournalsReady?.();
11452
+ await microtasks();
11453
+ watcher.emit(undefined, {
11454
+ paths: new Map([[
11455
+ "/tmp/hq/companies/indigo/watcher-arrived-during-pull.md",
11456
+ "companies/indigo/watcher-arrived-during-pull.md",
11457
+ ]]),
11458
+ changes: new Map([[
11459
+ "/tmp/hq/companies/indigo/watcher-arrived-during-pull.md",
11460
+ { kind: "change" },
11461
+ ]]),
11462
+ });
11463
+ // Let the watcher submit its realtime work before the full pass reaches
11464
+ // its explicit hand-off. No timer is involved: the submission is a
11465
+ // promise-only scheduling boundary.
11466
+ await microtasks();
11467
+ onAppliedUpserts?.([{
11468
+ relativePath: "companies/indigo/initial-pull.md",
11469
+ contentHash: "sha256:initial-pull",
11470
+ }]);
11471
+ await checkpoint?.();
11472
+ return {
11473
+ exitCode: 0,
11474
+ result: {
11475
+ pushPathResults: [],
11476
+ appliedUpserts: [{
11477
+ relativePath: "companies/indigo/initial-pull.md",
11478
+ contentHash: "sha256:initial-pull",
11479
+ }],
11480
+ },
11481
+ };
11482
+ });
11483
+ const { loop, shutdown } = runLoop({
11484
+ claims: EMAIL_CLAIMS,
11485
+ startEventSync,
11486
+ runPass,
11487
+ watcher,
11488
+ supportsCooperativeCheckpoint: true,
11489
+ });
11490
+ await microtasks(16);
11491
+ expect(suppressionWasInstalledWhenRealtimeStarted).toBe(true);
11492
+ shutdown();
11493
+ await loop;
11494
+ });
11495
+ it("does not let a yielding warm-up catch-up overwrite a newer watcher unlink", async () => {
11496
+ const watcher = makeBatchWatcherStub();
11497
+ let resolveCatchup;
11498
+ watcher.collectWarmupCatchupAsync = vi.fn(() => new Promise((resolve) => {
11499
+ resolveCatchup = resolve;
11500
+ }));
11501
+ const runPass = vi.fn(async (_argv, _checkpoint, onJournalsReady) => {
11502
+ onJournalsReady?.();
11503
+ return completedPassOutcome();
11504
+ });
11505
+ const { loop, tick, shutdown } = runLoop({
11506
+ claims: EMAIL_CLAIMS,
11507
+ watcher,
11508
+ runPass,
11509
+ supportsCooperativeCheckpoint: true,
11510
+ });
11511
+ await microtasks(12);
11512
+ const absolutePath = "/tmp/hq/companies/indigo/recreated.md";
11513
+ const relativePath = "companies/indigo/recreated.md";
11514
+ watcher.emit(undefined, {
11515
+ paths: new Map([[absolutePath, relativePath]]),
11516
+ changes: new Map([[absolutePath, {
11517
+ kind: "unlink",
11518
+ deleteSnapshots: [{
11519
+ journalSlug: "indigo",
11520
+ journalPath: "recreated.md",
11521
+ deleteScopeRoot: "recreated.md",
11522
+ absolutePath,
11523
+ remoteEtag: "etag-before-recreate",
11524
+ localHash: "hash-before-recreate",
11525
+ localKind: "file",
11526
+ }],
11527
+ }]]),
11528
+ });
11529
+ // This is the older filesystem state collected before the live unlink.
11530
+ resolveCatchup?.({
11531
+ batch: {
11532
+ paths: new Map([[absolutePath, relativePath]]),
11533
+ changes: new Map([[absolutePath, { kind: "change" }]]),
11534
+ },
11535
+ scannedPaths: 1,
11536
+ });
11537
+ await microtasks(16);
11538
+ tick();
11539
+ await microtasks(16);
11540
+ const scopedPush = runPass.mock.calls.find((call) => call[0].includes("--scope-path"))?.[0];
11541
+ expect(scopedPush).toEqual(expect.arrayContaining([
11542
+ "--scope-path",
11543
+ "recreated.md",
11544
+ "--delete-scope-path",
11545
+ "recreated.md",
11546
+ ]));
11547
+ shutdown();
11548
+ await loop;
11549
+ });
10228
11550
  it("replays pull upsert marks after event-sync startup becomes live", async () => {
10229
11551
  const markContentPublished = vi.fn();
10230
11552
  let resolveStart;
@@ -12615,6 +13937,45 @@ describe("watcher delete intents without pre-seeded authorization", () => {
12615
13937
  expect(Object.keys(readJournal("indigo").files)).toHaveLength(12);
12616
13938
  await stopHarness(h);
12617
13939
  });
13940
+ it("attributes a company-root unlinkDir to its journal while rejecting company-root metadata files", async () => {
13941
+ const keys = Array.from({ length: 12 }, (_, i) => `knowledge/company-root/f-${i}.md`);
13942
+ const pushPasses = [];
13943
+ const h = startDeleteHarness(keys, {
13944
+ direction: "push",
13945
+ onPass: (argv) => {
13946
+ if (argv[argv.indexOf("--direction") + 1] === "push") {
13947
+ pushPasses.push(argv);
13948
+ }
13949
+ },
13950
+ });
13951
+ await settle();
13952
+ pushPasses.length = 0;
13953
+ const childKey = keys[0];
13954
+ const childPath = path.join(h.companyRoot, childKey);
13955
+ fs.unlinkSync(childPath);
13956
+ expect(h.capture()(`companies/indigo/${childKey}`, "unlink")).toHaveLength(1);
13957
+ expect(readJournal("indigo").files[childKey]?.localDeleteIntent).toBeDefined();
13958
+ // The child intent is safe only until the enclosing whole-company removal
13959
+ // is classified. At that scope the exact denominator is the whole shard,
13960
+ // so the existing bulk guard must revoke it rather than lose attribution.
13961
+ fs.rmSync(h.companyRoot, { recursive: true, force: true });
13962
+ const rootSnapshots = h.capture()("companies/indigo", "unlinkDir");
13963
+ expect(rootSnapshots).toHaveLength(0);
13964
+ expect(Object.values(readJournal("indigo").files).filter((entry) => entry.localDeleteIntent)).toHaveLength(0);
13965
+ h.watcher.emit("companies/indigo", {
13966
+ paths: new Map([[h.companyRoot, "companies/indigo"]]),
13967
+ changes: new Map([[h.companyRoot, {
13968
+ kind: "unlinkDir",
13969
+ deleteSnapshots: rootSnapshots,
13970
+ }]]),
13971
+ });
13972
+ h.tick();
13973
+ await settle();
13974
+ expect(pushPasses).toEqual([
13975
+ expect.arrayContaining(["--company", "indigo", "--direction", "push"]),
13976
+ ]);
13977
+ await stopHarness(h);
13978
+ });
12618
13979
  // The native macOS/Windows backends make no child-before-parent guarantee,
12619
13980
  // so the refusal has to hold for descendants that arrive after it too.
12620
13981
  it("a descendant unlink arriving after a refused unlinkDir records no intent", async () => {