@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,12 +10,12 @@ import { pickCanonicalPersonEntity } from "../vault-client.js";
10
10
  import { acquireOperationLock, withOperationLock, withOperationLockSync, OperationLockedError, OPERATION_LOCKED_EXIT, } from "../operation-lock.js";
11
11
  import { describeError } from "../lib/describe-error.js";
12
12
  import { TRANSIENT_NETWORK_EXIT } from "../lib/net-errors.js";
13
- import { PARTIAL_SYNC_EXIT } from "../lib/exit-codes.js";
13
+ import { PARTIAL_SYNC_EXIT, UNRESOLVED_COMPANY_EXIT, } from "../lib/exit-codes.js";
14
14
  import { getOrCreateMachineId } from "../lib/machine-id.js";
15
15
  import { localPathForVaultKey } from "../local-path-codec.js";
16
16
  import { TreeWatcher, resolveEventDebounceConfig, systemClock, } from "../watcher.js";
17
17
  import { recoverHqRootLocalState } from "../hq-root-recovery.js";
18
- import { clearLocalDeleteIntent, getStateDir, markLocalDeleteIntent, PERSONAL_VAULT_JOURNAL_SLUG, openJournalStoreSession, openJournalStoreSessionAsync, listJournalSlugs, listJournals, maintainAreaJournalSlice, readJournal, releaseCachedAreaJournalMigrations, journalCacheCensusSizes, journalRows, readJournalScoped, writeJournal, } from "../journal.js";
18
+ import { clearLocalDeleteIntent, getStateDir, markLocalDeleteIntent, PERSONAL_VAULT_JOURNAL_SLUG, openJournalStoreSession, openJournalStoreSessionAsync, listJournalSlugs, listJournals, maintainAreaJournalSlice, disposeScopedJournalStores, releaseScopedJournalStore, releaseCachedAreaJournalMigrations, journalCacheCensusSizes, journalRows, readJournalScoped, writeJournal, } from "../journal.js";
19
19
  import { isPersonalVaultDeletionExcluded } from "../personal-vault.js";
20
20
  import { NoopPushReceiver, } from "../sync/push-receiver.js";
21
21
  import { resolveEventSync, startEventSync as defaultStartEventSync, } from "../sync/event-sync.js";
@@ -31,6 +31,219 @@ import { InlineSyncWorkerAdapter, } from "../sync/sync-work.js";
31
31
  import { buildRoutePullArgv, buildScopedPushArgv, buildScopedDrainPullArgv, buildTargetedPullArgv, isWatchRouteSelected, routeChangeToTarget, routeKey, } from "./sync-runner-watch-routes.js";
32
32
  import { resolveSkipCompanies } from "./sync-runner-planning.js";
33
33
  const FULL_RECONCILE_EVERY_TICKS = 10;
34
+ /** A deferred full chore must get the next admission after this long. */
35
+ export const CHORE_STARVATION_CAP_MS = 15 * 60 * 1000;
36
+ /**
37
+ * One durable-state owner at a time for a full chore. Realtime work never runs
38
+ * beside that chore: it explicitly awaits `checkpoint()` after a completed
39
+ * durable unit, then resumes once the hand-off is finished. A separately
40
+ * tracked, explicitly opted-in watcher fast pass may use the pre-existing
41
+ * scoped fast-over-slow exception, but never while a chore owns durable state.
42
+ */
43
+ export class CooperativePassScheduler {
44
+ now;
45
+ starvationCapMs;
46
+ pending = [];
47
+ active = null;
48
+ // Scoped fast realtime work has the established in-process lock exception
49
+ // while a scoped slow realtime upload is in flight. Keep that slot separate
50
+ // from `active`: a chore still has exactly one durable-state owner.
51
+ activeFastRealtime = null;
52
+ stopped = false;
53
+ _handoffActive = false;
54
+ constructor(now, starvationCapMs = CHORE_STARVATION_CAP_MS) {
55
+ this.now = now;
56
+ this.starvationCapMs = starvationCapMs;
57
+ }
58
+ get handoffActive() {
59
+ return this._handoffActive;
60
+ }
61
+ get hasActiveChore() {
62
+ return this.active?.workClass === "chore";
63
+ }
64
+ get hasActiveSlowRealtime() {
65
+ return this.active?.workClass === "realtime" && this.active.lane === "slow";
66
+ }
67
+ pendingLaneCount(lane) {
68
+ return this.pending.filter((entry) => entry.lane === lane).length;
69
+ }
70
+ submit(task, workClass, lane = "slow", allowFastRealtimeOvertake = false) {
71
+ if (this.stopped)
72
+ return Promise.resolve(0);
73
+ const directEntry = {
74
+ task,
75
+ workClass,
76
+ lane,
77
+ allowFastRealtimeOvertake,
78
+ // An immediately admitted task cannot starve, so avoid sampling the
79
+ // cadence clock merely to stamp it. This preserves the loop's deadline
80
+ // accounting and keeps the timestamp meaningful only for real queues.
81
+ enqueuedAt: 0,
82
+ resolve: () => undefined,
83
+ reject: () => undefined,
84
+ };
85
+ if (this.canStartFastRealtime(directEntry)) {
86
+ return this.startFastRealtime(directEntry);
87
+ }
88
+ if (this.active === null &&
89
+ this.activeFastRealtime === null &&
90
+ this.pending.length === 0) {
91
+ return this.start(directEntry);
92
+ }
93
+ return new Promise((resolve, reject) => {
94
+ const entry = { ...directEntry, enqueuedAt: this.now(), resolve, reject };
95
+ this.pending.push(entry);
96
+ this.drain();
97
+ });
98
+ }
99
+ stop() {
100
+ this.stopped = true;
101
+ while (this.pending.length > 0)
102
+ this.pending.shift()?.resolve(0);
103
+ }
104
+ drain() {
105
+ if (this.stopped || this.pending.length === 0)
106
+ return;
107
+ if (this.active !== null) {
108
+ const fastRealtime = this.nextFastRealtime();
109
+ if (fastRealtime && this.canStartFastRealtime(fastRealtime)) {
110
+ this.pending.splice(this.pending.indexOf(fastRealtime), 1);
111
+ this.startFastRealtime(fastRealtime);
112
+ }
113
+ return;
114
+ }
115
+ if (this.activeFastRealtime !== null)
116
+ return;
117
+ const next = this.takeNext();
118
+ if (next)
119
+ this.start(next);
120
+ }
121
+ takeNext() {
122
+ const now = this.now();
123
+ const starvedChore = this.pending.findIndex((entry) => entry.workClass === "chore" && now - entry.enqueuedAt >= this.starvationCapMs);
124
+ if (starvedChore >= 0)
125
+ return this.pending.splice(starvedChore, 1)[0];
126
+ // A receiver's bulk drain is still realtime work, but its slow lane is the
127
+ // progress boundary for that delivery. Preserve its established turn
128
+ // before refilling later fast realtime arrivals; this does not let a
129
+ // chore overtake any realtime work.
130
+ const realtime = this.nextRealtimeIndex();
131
+ if (realtime >= 0)
132
+ return this.pending.splice(realtime, 1)[0];
133
+ return this.pending.shift();
134
+ }
135
+ nextRealtimeIndex() {
136
+ const slowRealtime = this.pending.findIndex((entry) => entry.workClass === "realtime" && entry.lane === "slow");
137
+ return slowRealtime >= 0
138
+ ? slowRealtime
139
+ : this.pending.findIndex((entry) => entry.workClass === "realtime");
140
+ }
141
+ nextFastRealtime() {
142
+ return this.pending.find((entry) => entry.workClass === "realtime" && entry.lane === "fast");
143
+ }
144
+ hasStarvedChore() {
145
+ const now = this.now();
146
+ return this.pending.some((entry) => entry.workClass === "chore" && now - entry.enqueuedAt >= this.starvationCapMs);
147
+ }
148
+ canStartFastRealtime(entry) {
149
+ return (!this._handoffActive &&
150
+ this.activeFastRealtime === null &&
151
+ entry.workClass === "realtime" &&
152
+ entry.lane === "fast" &&
153
+ entry.allowFastRealtimeOvertake &&
154
+ this.hasActiveSlowRealtime &&
155
+ !this.hasStarvedChore());
156
+ }
157
+ start(entry) {
158
+ this.active = entry;
159
+ let current;
160
+ try {
161
+ current = this.execute(entry);
162
+ }
163
+ catch (error) {
164
+ current = Promise.reject(error);
165
+ }
166
+ void current.then((outcome) => {
167
+ entry.resolve(outcome);
168
+ this.complete(entry);
169
+ }, (error) => {
170
+ entry.reject(error);
171
+ this.complete(entry);
172
+ }).catch(() => undefined);
173
+ return current;
174
+ }
175
+ complete(entry) {
176
+ if (this.active === entry)
177
+ this.active = null;
178
+ this.drain();
179
+ }
180
+ startFastRealtime(entry) {
181
+ this.activeFastRealtime = entry;
182
+ let current;
183
+ try {
184
+ current = this.execute(entry);
185
+ }
186
+ catch (error) {
187
+ current = Promise.reject(error);
188
+ }
189
+ void current.then((outcome) => {
190
+ entry.resolve(outcome);
191
+ this.completeFastRealtime(entry);
192
+ }, (error) => {
193
+ entry.reject(error);
194
+ this.completeFastRealtime(entry);
195
+ });
196
+ return current;
197
+ }
198
+ completeFastRealtime(entry) {
199
+ if (this.activeFastRealtime === entry)
200
+ this.activeFastRealtime = null;
201
+ this.drain();
202
+ }
203
+ execute(entry) {
204
+ return entry.task({ checkpoint: () => this.checkpoint(entry) });
205
+ }
206
+ async checkpoint(entry) {
207
+ if (this.stopped ||
208
+ entry.workClass !== "chore" ||
209
+ this.active !== entry ||
210
+ !this.pending.some((pending) => pending.workClass === "realtime")) {
211
+ return false;
212
+ }
213
+ // `entry` is suspended at a caller-declared durable boundary. It remains
214
+ // the sole full-pass continuation and will resume after this bounded burst.
215
+ const deferredAt = this.now();
216
+ this.active = null;
217
+ this._handoffActive = true;
218
+ let handedOff = false;
219
+ try {
220
+ while (!this.stopped && this.now() - deferredAt < this.starvationCapMs) {
221
+ const realtimeIndex = this.nextRealtimeIndex();
222
+ if (realtimeIndex < 0)
223
+ break;
224
+ const realtime = this.pending.splice(realtimeIndex, 1)[0];
225
+ handedOff = true;
226
+ this.active = realtime;
227
+ try {
228
+ realtime.resolve(await this.execute(realtime));
229
+ }
230
+ catch (error) {
231
+ realtime.reject(error);
232
+ }
233
+ finally {
234
+ if (this.active === realtime)
235
+ this.active = null;
236
+ }
237
+ }
238
+ }
239
+ finally {
240
+ this._handoffActive = false;
241
+ if (!this.stopped)
242
+ this.active = entry;
243
+ }
244
+ return handedOff;
245
+ }
246
+ }
34
247
  const MAX_FULL_RECONCILE_REASONS = 7;
35
248
  /**
36
249
  * Area storage is AUTO by default and only applies after durable cutover.
@@ -510,6 +723,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
510
723
  const idleHeartbeatIntervalMs = deps.idleHeartbeatIntervalMs ?? WATCH_IDLE_HEARTBEAT_INTERVAL_MS;
511
724
  const monotonicNow = deps.monotonicNow ?? (() => performance.now());
512
725
  const runPass = deps.runPass ?? runtime.runPassWithOperationLockAlreadyHeld;
726
+ const runPassSupportsCooperativeCheckpoint = deps.runPass === undefined || deps.runPassSupportsCooperativeCheckpoint === true;
513
727
  const pollIdx = argv.indexOf("--poll-remote-ms");
514
728
  // An explicit --poll-remote-ms pins a fixed interval (menubar/CLI still set
515
729
  // one). When it is omitted, the interval is resolved fresh each cycle from
@@ -520,7 +734,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
520
734
  const eventPush = argv.includes("--event-push");
521
735
  const fullReconcileIntervalMs = resolveFullReconcileMs();
522
736
  const companiesMode = argv.includes("--companies");
523
- const companyMode = argv.includes("--company");
737
+ const companyIdx = argv.indexOf("--company");
738
+ const companyMode = companyIdx >= 0;
739
+ // The tenant a single-company run owns. Carried into route selection so a
740
+ // watcher or receiver event under another company cannot queue a pass for it.
741
+ const companySlug = companyMode ? argv[companyIdx + 1] : undefined;
524
742
  const personalMode = argv.includes("--personal");
525
743
  // Keep this at the watcher ingress rather than importing the runner's
526
744
  // resolver: sync-runner imports this loop, and a reverse value import would
@@ -529,6 +747,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
529
747
  const watchRouteSelection = {
530
748
  companies: companiesMode,
531
749
  company: companyMode,
750
+ ...(companySlug !== undefined ? { companySlug } : {}),
532
751
  personal: personalMode,
533
752
  skipPersonal: parsed.skipPersonal === true ||
534
753
  skipPersonalEnv === "1" ||
@@ -673,7 +892,27 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
673
892
  resolveNoOperationLockUsers = resolve;
674
893
  });
675
894
  };
676
- const runPassWithLock = async (passArgvForRun, prepare, lane = "slow") => {
895
+ const passScheduler = new CooperativePassScheduler(monotonicNow);
896
+ const runPassWithLock = async (passArgvForRun, prepare, lane = "slow", workClass = "chore", checkpoint) => {
897
+ const reportedAppliedUpserts = new Set();
898
+ const recordDurableAppliedUpserts = (upserts) => {
899
+ for (const applied of upserts) {
900
+ const key = `${applied.relativePath}\u0000${applied.contentHash}`;
901
+ if (reportedAppliedUpserts.has(key))
902
+ continue;
903
+ reportedAppliedUpserts.add(key);
904
+ rememberAppliedUpsert(applied.relativePath, applied.contentHash);
905
+ }
906
+ };
907
+ const invokePass = () => {
908
+ if (!runPassSupportsCooperativeCheckpoint)
909
+ return runPass(passArgvForRun);
910
+ // This callback is emitted by the production runner after journal
911
+ // repair/seed and before fanout. It starts ingress early, but never
912
+ // admits a second durable writer: queued realtime still waits for the
913
+ // existing scheduler's cooperative hand-off checkpoint.
914
+ return runPass(passArgvForRun, checkpoint, () => activateEventPushSurfaces(), recordDurableAppliedUpserts);
915
+ };
677
916
  // Observe the heap at pass boundaries — a gated checkpoint immediately before
678
917
  // admitting a pass and immediately after it returns. A pass that runs a long
679
918
  // synchronous stretch starves the interval timer, so these boundary
@@ -685,9 +924,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
685
924
  heapCheckpoint?.();
686
925
  if (typeof outcome === "number")
687
926
  return;
688
- for (const applied of outcome.result?.appliedUpserts ?? []) {
689
- rememberAppliedUpsert(applied.relativePath, applied.contentHash);
690
- }
927
+ recordDurableAppliedUpserts(outcome.result?.appliedUpserts ?? []);
691
928
  };
692
929
  // Pre-admit boundary observation.
693
930
  heapCheckpoint?.();
@@ -696,13 +933,31 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
696
933
  // its keyed journal delta and per-object mutation are independent from the
697
934
  // slow pass's bulk paths, and the slow pass already owns the root lock.
698
935
  // Never bypass the lock when no slow pass is active.
936
+ if (
937
+ // A full chore is paused at a caller-declared durable checkpoint. Keep
938
+ // the root lock held by this process, but lend it to exactly one
939
+ // realtime pass; the suspended chore cannot mutate until this returns.
940
+ workClass === "realtime" &&
941
+ passScheduler.handoffActive &&
942
+ processOwnsOperationLock) {
943
+ acquireOperationLockUser();
944
+ try {
945
+ prepare?.();
946
+ const outcome = await invokePass();
947
+ recordAppliedUpserts(outcome);
948
+ return outcome;
949
+ }
950
+ finally {
951
+ releaseOperationLockUser();
952
+ }
953
+ }
699
954
  if (lane === "fast" &&
700
955
  processOwnsOperationLock &&
701
956
  coordinatorWholeMutationChores === 0) {
702
957
  acquireOperationLockUser();
703
958
  try {
704
959
  prepare?.();
705
- const outcome = await runPass(passArgvForRun);
960
+ const outcome = await invokePass();
706
961
  recordAppliedUpserts(outcome);
707
962
  return outcome;
708
963
  }
@@ -722,7 +977,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
722
977
  acquireOperationLockUser();
723
978
  }
724
979
  prepare?.();
725
- const outcome = await runPass(passArgvForRun);
980
+ const outcome = await invokePass();
726
981
  recordAppliedUpserts(outcome);
727
982
  return outcome;
728
983
  }
@@ -756,7 +1011,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
756
1011
  }
757
1012
  try {
758
1013
  prepare?.();
759
- const outcome = await runPass(passArgvForRun);
1014
+ const outcome = await invokePass();
760
1015
  recordAppliedUpserts(outcome);
761
1016
  return outcome;
762
1017
  }
@@ -779,7 +1034,18 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
779
1034
  throw err;
780
1035
  }
781
1036
  };
782
- const runTaskWithLock = async (task) => {
1037
+ const runTaskWithLock = async (task, workClass = "chore") => {
1038
+ if (workClass === "realtime" &&
1039
+ passScheduler.handoffActive &&
1040
+ processOwnsOperationLock) {
1041
+ acquireOperationLockUser();
1042
+ try {
1043
+ return await task();
1044
+ }
1045
+ finally {
1046
+ releaseOperationLockUser();
1047
+ }
1048
+ }
783
1049
  try {
784
1050
  const handle = acquireOperationLock(hqRoot, "sync", {
785
1051
  timeoutSec: 0,
@@ -873,102 +1139,15 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
873
1139
  heapCheckpoint?.();
874
1140
  }
875
1141
  };
876
- let activeSlowPass = null;
877
- let activeFastPass = null;
878
- const pendingSlowPasses = [];
879
- const pendingFastPasses = [];
880
1142
  const resolveStoppedQueue = () => {
881
- for (const queue of [pendingFastPasses, pendingSlowPasses]) {
882
- while (queue.length > 0)
883
- queue.shift()?.resolve(0);
884
- }
1143
+ passScheduler.stop();
885
1144
  };
886
- const drainQueuedPasses = () => {
887
- if (stopped) {
888
- resolveStoppedQueue();
889
- return;
890
- }
891
- // A queued slow pass is the bulk-drain progress boundary. Admit it before
892
- // refilling the realtime queue; otherwise a steady stream of fast work
893
- // perpetually leapfrogs a receiver's bulk batch and its SQS poll loop
894
- // cannot issue another receive.
895
- if (activeSlowPass === null &&
896
- activeFastPass === null &&
897
- pendingSlowPasses.length > 0) {
898
- const slow = pendingSlowPasses.shift();
899
- if (slow) {
900
- const current = startGuardedTask(slow.task, "slow");
901
- void current.then(slow.resolve, slow.reject);
902
- }
903
- }
904
- if (activeFastPass === null) {
905
- const fast = pendingFastPasses.shift();
906
- if (fast) {
907
- const current = startGuardedTask(fast.task, "fast");
908
- void current.then(fast.resolve, fast.reject);
909
- }
910
- }
911
- // A fast pass that acquired the operation lock normally (rather than
912
- // bypassing an in-process slow holder) still owns the cross-process
913
- // boundary. Slow work must queue behind it; only fast work is allowed to
914
- // overlap an already-lock-owning slow pass.
915
- if (activeSlowPass === null && activeFastPass === null) {
916
- const slow = pendingSlowPasses.shift();
917
- if (slow) {
918
- const current = startGuardedTask(slow.task, "slow");
919
- void current.then(slow.resolve, slow.reject);
920
- }
921
- }
922
- };
923
- const startGuardedTask = (task, lane) => {
924
- // Claim the lane before invoking `task`: a task can synchronously activate
925
- // a watcher/scheduler callback before returning its promise. Without this
926
- // placeholder that re-entrant callback sees an idle lane and starts a
927
- // second lock contender.
928
- const placeholder = Promise.resolve(0);
929
- if (lane === "fast")
930
- activeFastPass = placeholder;
931
- else
932
- activeSlowPass = placeholder;
933
- let current;
934
- try {
935
- current = stopped ? placeholder : task();
936
- }
937
- catch (err) {
938
- current = Promise.reject(err);
939
- }
940
- if (lane === "fast")
941
- activeFastPass = current;
942
- else
943
- activeSlowPass = current;
944
- void current
945
- .finally(() => {
946
- if (lane === "fast" && activeFastPass === current) {
947
- activeFastPass = null;
948
- drainQueuedPasses();
949
- }
950
- else if (lane === "slow" && activeSlowPass === current) {
951
- activeSlowPass = null;
952
- drainQueuedPasses();
953
- }
954
- })
955
- .catch(() => undefined);
956
- return current;
957
- };
958
- const runGuardedTask = (task, lane = "slow") => {
959
- const active = lane === "fast"
960
- ? activeFastPass
961
- : activeSlowPass ?? activeFastPass;
962
- const queued = lane === "fast" ? pendingFastPasses : pendingSlowPasses;
963
- if (active === null && queued.length === 0) {
964
- return startGuardedTask(task, lane);
965
- }
966
- return new Promise((resolve, reject) => {
967
- queued.push({ task, lane, resolve, reject });
968
- drainQueuedPasses();
969
- });
970
- };
971
- const runGuarded = (passArgvForRun, prepare, lane = "slow") => runGuardedTask(() => runPassWithLock(passArgvForRun, prepare, lane), lane);
1145
+ const runGuardedTask = (task, lane = "slow", workClass = "chore", allowFastRealtimeOvertake = false) =>
1146
+ // Lanes still choose the pass execution profile, but no longer decide
1147
+ // chore ownership. Realtime normally waits for a chore hand-off; the
1148
+ // watcher fast path explicitly opts into its established scoped exception.
1149
+ passScheduler.submit(task, workClass, lane, allowFastRealtimeOvertake);
1150
+ const runGuarded = (passArgvForRun, prepare, lane = "slow", workClass = "chore", allowFastRealtimeOvertake = false) => runGuardedTask((context) => runPassWithLock(passArgvForRun, prepare, lane, workClass, workClass === "chore" ? context.checkpoint : undefined), lane, workClass, allowFastRealtimeOvertake);
972
1151
  /*
973
1152
  * Area storage and the parallel coordinator default to AUTO. AUTO turns
974
1153
  * them on after durable personal-vault cutover; explicit off is the kill
@@ -1160,6 +1339,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1160
1339
  });
1161
1340
  },
1162
1341
  onCutOver: () => {
1342
+ releaseScopedJournalStore(PERSONAL_VAULT_JOURNAL_SLUG);
1163
1343
  parallelCoordinatorEnabled = refreshCoordinatorEnabled();
1164
1344
  if (parallelCoordinatorEnabled && coordinator === null) {
1165
1345
  coordinator = createCoordinator();
@@ -1267,6 +1447,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1267
1447
  let drainedUntrackedDeletes = 0;
1268
1448
  const pendingWatcherPaths = new Map();
1269
1449
  const pendingWatcherChanges = new Map();
1450
+ // A warm-up scan observes filesystem state over time. Keep an ingress
1451
+ // sequence so its older observations cannot overwrite a live watcher event
1452
+ // that arrived while the scan was yielding.
1453
+ const pendingWatcherChangeSequences = new Map();
1454
+ let nextPendingWatcherChangeSequence = 0;
1270
1455
  let pendingWatcherOriginalBatch = null;
1271
1456
  // Every coalesced watcher batch gets one durable in-process generation.
1272
1457
  // A wake can cause the scheduler to revisit a drained batch, but an event
@@ -1290,11 +1475,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1290
1475
  const paths = new Map();
1291
1476
  const changes = new Map();
1292
1477
  for (const [absolutePath, relativePath] of batch.paths.entries()) {
1293
- const route = routeChangeToTarget(relativePath);
1478
+ const change = batch.changes?.get(absolutePath);
1479
+ const route = routeChangeToTarget(relativePath, change?.kind);
1294
1480
  if (!route || !isWatchRouteSelected(route, watchRouteSelection))
1295
1481
  continue;
1296
1482
  paths.set(absolutePath, relativePath);
1297
- const change = batch.changes?.get(absolutePath);
1298
1483
  if (change)
1299
1484
  changes.set(absolutePath, change);
1300
1485
  }
@@ -1302,7 +1487,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1302
1487
  return null;
1303
1488
  return { ...batch, paths, changes };
1304
1489
  };
1305
- const addPendingWatcherChange = (changedRelPath, batch) => {
1490
+ const addPendingWatcherChange = (changedRelPath, batch, options) => {
1306
1491
  if (batch) {
1307
1492
  const selectedBatch = selectWatchBatch(batch);
1308
1493
  if (!selectedBatch)
@@ -1315,20 +1500,30 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1315
1500
  return false;
1316
1501
  }
1317
1502
  }
1318
- pendingWatcherBatchGeneration ??= ++nextWatcherBatchGeneration;
1319
1503
  if (batch) {
1504
+ const entries = [...batch.paths.entries()].filter(([absolutePath]) => {
1505
+ const previousSequence = pendingWatcherChangeSequences.get(absolutePath);
1506
+ return previousSequence === undefined ||
1507
+ options?.preserveChangesAfter === undefined ||
1508
+ previousSequence <= options.preserveChangesAfter;
1509
+ });
1510
+ if (entries.length === 0)
1511
+ return false;
1512
+ pendingWatcherBatchGeneration ??= ++nextWatcherBatchGeneration;
1320
1513
  pendingWatcherOriginalBatch =
1321
1514
  pendingWatcherPaths.size === 0 &&
1322
1515
  !pendingWatcherBareChange &&
1323
1516
  !pendingWatcherOverflowed &&
1324
- !batch.overflowed
1517
+ !batch.overflowed &&
1518
+ entries.length === batch.paths.size
1325
1519
  ? batch
1326
1520
  : null;
1327
- for (const [absolutePath, relativePath] of batch.paths.entries()) {
1521
+ for (const [absolutePath, relativePath] of entries) {
1328
1522
  pendingWatcherPaths.set(absolutePath, relativePath);
1329
1523
  const change = batch.changes?.get(absolutePath);
1330
1524
  if (change)
1331
1525
  pendingWatcherChanges.set(absolutePath, change);
1526
+ pendingWatcherChangeSequences.set(absolutePath, ++nextPendingWatcherChangeSequence);
1332
1527
  }
1333
1528
  if (batch.overflowed) {
1334
1529
  pendingWatcherOverflowed = true;
@@ -1345,6 +1540,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1345
1540
  }
1346
1541
  return true;
1347
1542
  }
1543
+ pendingWatcherBatchGeneration ??= ++nextWatcherBatchGeneration;
1348
1544
  if (changedRelPath) {
1349
1545
  pendingWatcherOriginalBatch = null;
1350
1546
  // Codec boundary: `changedRelPath` is a canonical vault key (the
@@ -1353,6 +1549,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1353
1549
  // point at the impossible colon path on Windows.
1354
1550
  const absolutePath = localPathForVaultKey(hqRoot, changedRelPath);
1355
1551
  pendingWatcherPaths.set(absolutePath, changedRelPath);
1552
+ pendingWatcherChangeSequences.set(absolutePath, ++nextPendingWatcherChangeSequence);
1356
1553
  // A bare path signal carries no delete intent; drop any change record
1357
1554
  // a prior batch left for this path so the stale intent cannot leak
1358
1555
  // into the coalesced batch (#227).
@@ -1394,6 +1591,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1394
1591
  const generation = pendingWatcherBatchGeneration;
1395
1592
  pendingWatcherPaths.clear();
1396
1593
  pendingWatcherChanges.clear();
1594
+ pendingWatcherChangeSequences.clear();
1397
1595
  pendingWatcherOriginalBatch = null;
1398
1596
  pendingWatcherBareChange = false;
1399
1597
  pendingWatcherWarmupCatchup = false;
@@ -1412,6 +1610,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1412
1610
  pendingWatcherOriginalBatch = null;
1413
1611
  for (const [absolutePath, relativePath] of paths.entries()) {
1414
1612
  pendingWatcherPaths.set(absolutePath, relativePath);
1613
+ pendingWatcherChangeSequences.set(absolutePath, ++nextPendingWatcherChangeSequence);
1415
1614
  }
1416
1615
  };
1417
1616
  /**
@@ -1492,8 +1691,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1492
1691
  }
1493
1692
  return null;
1494
1693
  };
1495
- const journalCoordinates = (relativePath) => {
1496
- const route = routeChangeToTarget(relativePath);
1694
+ const journalCoordinates = (relativePath, kind) => {
1695
+ const route = routeChangeToTarget(relativePath, kind);
1497
1696
  if (!route || !isWatchRouteSelected(route, watchRouteSelection))
1498
1697
  return null;
1499
1698
  if (route.kind === "personal") {
@@ -1528,7 +1727,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1528
1727
  declaredSize >= 0) {
1529
1728
  return declaredSize;
1530
1729
  }
1531
- const size = journalRows(readJournal(coordinates.slug)).get(coordinates.key)?.size;
1730
+ const size = journalRows(readJournalScoped(coordinates.slug, [coordinates.key]))
1731
+ .get(coordinates.key)?.size;
1532
1732
  return typeof size === "number" && Number.isSafeInteger(size) && size >= 0 ? size : null;
1533
1733
  };
1534
1734
  /**
@@ -1575,8 +1775,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1575
1775
  return session.get(coordinates.key) === undefined;
1576
1776
  };
1577
1777
  const captureLocalDeleteSnapshots = (relativePath, kind) => {
1578
- const coordinates = journalCoordinates(relativePath);
1579
- const route = routeChangeToTarget(relativePath);
1778
+ const coordinates = journalCoordinates(relativePath, kind);
1779
+ const route = routeChangeToTarget(relativePath, kind);
1580
1780
  if (!coordinates || !route)
1581
1781
  return [];
1582
1782
  // Desktop workspace Off toggles are authoritative before journal I/O.
@@ -1697,8 +1897,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1697
1897
  * it scopes a push or pull for the affected batch.
1698
1898
  */
1699
1899
  const captureQueuedLocalDeleteSnapshots = async (relativePath, kind, isCurrent) => {
1700
- const coordinates = journalCoordinates(relativePath);
1701
- const route = routeChangeToTarget(relativePath);
1900
+ const coordinates = journalCoordinates(relativePath, kind);
1901
+ const route = routeChangeToTarget(relativePath, kind);
1702
1902
  if (!coordinates || !route)
1703
1903
  return [];
1704
1904
  if (route.kind === "company" &&
@@ -1792,8 +1992,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1792
1992
  }
1793
1993
  return snapshots;
1794
1994
  };
1795
- const warmupJournalPaths = () => {
1796
- const paths = [];
1995
+ const warmupJournalPaths = function* () {
1797
1996
  const journals = listJournals();
1798
1997
  const vaultJournalPresent = journals.some(({ slug }) => slug === PERSONAL_VAULT_JOURNAL_SLUG);
1799
1998
  for (const { slug, journal } of journals) {
@@ -1826,10 +2025,9 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1826
2025
  // ~12.8k of them per runner start after a transcript prune.
1827
2026
  if (route.kind === "personal" && isPersonalVaultDeletionExcluded(key))
1828
2027
  continue;
1829
- paths.push(route.kind === "personal" ? key : `companies/${route.slug}/${key}`);
2028
+ yield route.kind === "personal" ? key : `companies/${route.slug}/${key}`;
1830
2029
  }
1831
2030
  }
1832
- return paths;
1833
2031
  };
1834
2032
  const publishedContentHashLookup = (relativePath) => {
1835
2033
  const coordinates = journalCoordinates(relativePath);
@@ -1842,13 +2040,34 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1842
2040
  return publishedContentHashFromJournalEntryBeforeSession(entry, serviceStartedAt);
1843
2041
  };
1844
2042
  const prepareWatcherChanges = (changes) => {
2043
+ const keysByJournal = new Map();
2044
+ const rememberKey = (slug, key) => {
2045
+ const keys = keysByJournal.get(slug) ?? new Set();
2046
+ keys.add(key);
2047
+ keysByJournal.set(slug, keys);
2048
+ };
2049
+ for (const [absolutePath, change] of changes.entries()) {
2050
+ if (change.kind === "unlink" || change.kind === "unlinkDir") {
2051
+ for (const snapshot of change.deleteSnapshots ?? []) {
2052
+ rememberKey(snapshot.journalSlug, snapshot.journalPath);
2053
+ }
2054
+ continue;
2055
+ }
2056
+ const relativePath = path
2057
+ .relative(hqRoot, absolutePath)
2058
+ .split(path.sep)
2059
+ .join("/");
2060
+ const coordinates = journalCoordinates(relativePath);
2061
+ if (coordinates)
2062
+ rememberKey(coordinates.slug, coordinates.key);
2063
+ }
1845
2064
  const journals = new Map();
1846
2065
  const dirty = new Set();
1847
2066
  const journalFor = (slug) => {
1848
2067
  const loaded = journals.get(slug);
1849
2068
  if (loaded)
1850
2069
  return loaded;
1851
- const journal = readJournal(slug);
2070
+ const journal = readJournalScoped(slug, [...(keysByJournal.get(slug) ?? [])]);
1852
2071
  journals.set(slug, journal);
1853
2072
  return journal;
1854
2073
  };
@@ -1906,7 +2125,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1906
2125
  const groupBatchByRoute = (batch) => {
1907
2126
  const grouped = new Map();
1908
2127
  for (const [absolutePath, relativePath] of batch.paths.entries()) {
1909
- const route = routeChangeToTarget(relativePath);
2128
+ const change = batch.changes?.get(absolutePath);
2129
+ const route = routeChangeToTarget(relativePath, change?.kind);
1910
2130
  if (!route)
1911
2131
  continue;
1912
2132
  const key = routeKey(route);
@@ -1919,7 +2139,6 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1919
2139
  deleteScopeRoots: [],
1920
2140
  };
1921
2141
  existing.paths.set(absolutePath, relativePath);
1922
- const change = batch.changes?.get(absolutePath);
1923
2142
  if (change)
1924
2143
  existing.changes.set(absolutePath, change);
1925
2144
  existing.relPaths = [...new Set([...existing.paths.values()])];
@@ -2003,7 +2222,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
2003
2222
  }
2004
2223
  return false;
2005
2224
  };
2006
- const runScopedDrain = async (batch, watcherBatchGeneration, wakeOnPushFailure = false) => {
2225
+ const runScopedDrain = async (batch, watcherBatchGeneration, wakeOnPushFailure = false, allowFastRealtimeOvertake = false) => {
2007
2226
  if (batch)
2008
2227
  await resolveDeleteSnapshotCaptures(batch);
2009
2228
  const grouped = batch && batch.paths.size > 0 ? groupBatchByRoute(batch) : null;
@@ -2032,11 +2251,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
2032
2251
  const scopedArgv = buildScopedPushArgv(group.route, admittedPaths, passArgv, group.deleteScopeRoots);
2033
2252
  return runGuarded(scopedArgv, admittedChanges.size > 0
2034
2253
  ? () => prepareWatcherChanges(admittedChanges)
2035
- : undefined, lane);
2254
+ : undefined, lane, "realtime", allowFastRealtimeOvertake);
2036
2255
  })
2037
2256
  : { result: await runGuarded(buildScopedPushArgv(group.route, group.relPaths, passArgv, group.deleteScopeRoots), group.changes.size > 0
2038
2257
  ? () => prepareWatcherChanges(group.changes)
2039
- : undefined, lane), accepted: true, coordinatorRefusedPaths: [] };
2258
+ : undefined, lane, "realtime", allowFastRealtimeOvertake), accepted: true, coordinatorRefusedPaths: [] };
2040
2259
  const result = execution.result;
2041
2260
  if (execution.accepted && passExitCode(result) === 0) {
2042
2261
  const refusedPaths = new Set(typeof result === "number"
@@ -2106,8 +2325,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
2106
2325
  ? "fast"
2107
2326
  : "slow";
2108
2327
  const execution = parallelCoordinatorEnabled
2109
- ? await runRealtimePass("watcher", "pull", group.relPaths, lane, (slice) => runGuarded(buildTargetedPullArgv(group.route, passArgv, slice.paths.map((path) => path.canonicalPath)), undefined, lane))
2110
- : { result: await runGuarded(pullArgv, undefined, lane), accepted: true };
2328
+ ? await runRealtimePass("watcher", "pull", group.relPaths, lane, (slice) => runGuarded(buildTargetedPullArgv(group.route, passArgv, slice.paths.map((path) => path.canonicalPath)), undefined, lane, "realtime"))
2329
+ : { result: await runGuarded(pullArgv, undefined, lane, "realtime"), accepted: true };
2111
2330
  const exit = execution.accepted ? passExitCode(execution.result) : 1;
2112
2331
  if (exit !== 0 && worstExit === 0)
2113
2332
  worstExit = exit;
@@ -2312,13 +2531,13 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
2312
2531
  return;
2313
2532
  }
2314
2533
  if (lanes?.fast &&
2315
- activeSlowPass !== null) {
2534
+ (passScheduler.hasActiveChore || passScheduler.hasActiveSlowRealtime)) {
2316
2535
  const generation = ++nextWatcherBatchGeneration;
2317
2536
  // Fast dispatch bypasses the pending-batch ingress, so preserve both
2318
2537
  // of its contracts here: only selected routes may run, and V2 sees
2319
2538
  // the same dirty signal as the compatibility drain.
2320
2539
  realtimeState.scheduler?.signal("watcher");
2321
- void runScopedDrain(lanes.fast, generation, true).then((exit) => {
2540
+ void runScopedDrain(lanes.fast, generation, true, true).then((exit) => {
2322
2541
  if (exit === 0)
2323
2542
  return;
2324
2543
  restorePendingWatcherPaths(lanes.fast.paths, generation);
@@ -2373,29 +2592,36 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
2373
2592
  watcherRediscoveryRetries = 0;
2374
2593
  process.stderr.write(`hq-sync-runner: event-push watcher active (watched-paths=${watchedPaths ?? "unknown"})\n`);
2375
2594
  const startedAt = Date.now();
2376
- try {
2377
- const catchup = watcher.collectWarmupCatchup?.(serviceStartedAt - WATCHER_WARMUP_SAFETY_MARGIN_MS, warmupJournalPaths());
2378
- if (!catchup)
2379
- return;
2380
- const queued = addPendingWatcherChange(undefined, catchup.batch);
2381
- pendingWatcherWarmupCatchup ||= queued;
2382
- process.stderr.write(JSON.stringify({
2383
- type: "watcher-warmup-catchup",
2384
- reason: "watch-set-ready",
2385
- pathsFound: catchup.batch.paths.size,
2386
- scannedPaths: catchup.scannedPaths,
2387
- elapsedMs: Date.now() - startedAt,
2388
- queued,
2389
- }) + "\n");
2390
- if (queued)
2595
+ const liveChangeSequenceAtWarmupStart = nextPendingWatcherChangeSequence;
2596
+ void (async () => {
2597
+ try {
2598
+ const catchup = watcher?.collectWarmupCatchupAsync
2599
+ ? await watcher.collectWarmupCatchupAsync(serviceStartedAt - WATCHER_WARMUP_SAFETY_MARGIN_MS, warmupJournalPaths())
2600
+ : watcher?.collectWarmupCatchup?.(serviceStartedAt - WATCHER_WARMUP_SAFETY_MARGIN_MS, warmupJournalPaths());
2601
+ if (!catchup || catchup.cancelled || stopped || watcher === null)
2602
+ return;
2603
+ const queued = addPendingWatcherChange(undefined, catchup.batch, {
2604
+ preserveChangesAfter: liveChangeSequenceAtWarmupStart,
2605
+ });
2606
+ pendingWatcherWarmupCatchup ||= queued;
2607
+ process.stderr.write(JSON.stringify({
2608
+ type: "watcher-warmup-catchup",
2609
+ reason: "watch-set-ready",
2610
+ pathsFound: catchup.batch.paths.size,
2611
+ scannedPaths: catchup.scannedPaths,
2612
+ elapsedMs: Date.now() - startedAt,
2613
+ queued,
2614
+ }) + "\n");
2615
+ if (queued)
2616
+ interruptWait();
2617
+ }
2618
+ catch (err) {
2619
+ process.stderr.write(`hq-sync-runner: watcher warm-up catch-up failed: ${describeError(err)}\n`);
2620
+ // A scan failure must not silently extend the warm-up blind spot.
2621
+ pendingWatcherWarmupCatchupFailed = true;
2391
2622
  interruptWait();
2392
- }
2393
- catch (err) {
2394
- process.stderr.write(`hq-sync-runner: watcher warm-up catch-up failed: ${describeError(err)}\n`);
2395
- // A scan failure must not silently extend the warm-up blind spot.
2396
- pendingWatcherWarmupCatchupFailed = true;
2397
- interruptWait();
2398
- }
2623
+ }
2624
+ })();
2399
2625
  };
2400
2626
  if (watcher.onReady)
2401
2627
  watcher.onReady(reportWatcherReady);
@@ -2443,7 +2669,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
2443
2669
  // Submit every route in this lane before awaiting any result. In
2444
2670
  // particular, a later route's fast receipt must never wait for an
2445
2671
  // earlier route's slow pull to finish.
2446
- const completed = await Promise.all([...eventsByRoute.values()].map(({ route, events }) => runRealtimePass("receiver", "pull", events.map((event) => event.relativePath), lane, (slice) => runGuarded(buildTargetedPullArgv(route, passArgv, slice.paths.map((path) => path.canonicalPath)), undefined, lane))));
2672
+ const completed = await Promise.all([...eventsByRoute.values()].map(({ route, events }) => runRealtimePass("receiver", "pull", events.map((event) => event.relativePath), lane, (slice) => runGuarded(buildTargetedPullArgv(route, passArgv, slice.paths.map((path) => path.canonicalPath)), undefined, lane, "realtime"))));
2447
2673
  for (const execution of completed) {
2448
2674
  if (!execution.accepted || passExitCode(execution.result) !== 0) {
2449
2675
  throw new Error(`targeted pull failed with exit code ${passExitCode(execution.result)}`);
@@ -2497,7 +2723,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
2497
2723
  }
2498
2724
  for (const { route, paths } of pathsByRoute.values()) {
2499
2725
  const targetedArgv = buildTargetedPullArgv(route, passArgv, paths);
2500
- const result = await runGuarded(targetedArgv, undefined, lane);
2726
+ const result = await runGuarded(targetedArgv, undefined, lane, "realtime");
2501
2727
  if (passExitCode(result) !== 0) {
2502
2728
  throw new Error(`targeted pull failed with exit code ${passExitCode(result)}`);
2503
2729
  }
@@ -2545,14 +2771,14 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
2545
2771
  const execution = await runRealtimePass("wake", "pull", [], "slow", async () => runGuardedTask(() => runTaskWithLock(async () => {
2546
2772
  await drain();
2547
2773
  return 0;
2548
- })));
2774
+ }, "realtime"), "slow", "realtime"));
2549
2775
  outcome = execution.accepted ? execution.result : 1;
2550
2776
  }
2551
2777
  else {
2552
2778
  outcome = await runGuardedTask(() => runTaskWithLock(async () => {
2553
2779
  await drain();
2554
2780
  return 0;
2555
- }));
2781
+ }, "realtime"), "slow", "realtime");
2556
2782
  }
2557
2783
  }
2558
2784
  catch (err) {
@@ -2748,8 +2974,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
2748
2974
  targetedJournalSessions: () => targetedJournalSessionCensusSizes().targetedJournalSessions,
2749
2975
  targetedJournalRows: () => targetedJournalSessionCensusSizes().targetedJournalRows,
2750
2976
  cachedAreaJournalMigrations: () => journalCacheCensusSizes().cachedAreaJournalMigrations,
2751
- pendingSlowPasses: () => pendingSlowPasses.length,
2752
- pendingFastPasses: () => pendingFastPasses.length,
2977
+ pendingSlowPasses: () => passScheduler.pendingLaneCount("slow"),
2978
+ pendingFastPasses: () => passScheduler.pendingLaneCount("fast"),
2753
2979
  },
2754
2980
  eventSyncSizes: () => eventSync?.censusSizes?.(),
2755
2981
  watcherSizes: () => watcher?.censusSizes?.(),
@@ -3103,11 +3329,14 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3103
3329
  quarantinedDeleteRoots.delete(root);
3104
3330
  }
3105
3331
  // Delete snapshot capture needs a fresh keyed view while an episode is
3106
- // being assembled, but completed watch cycles do not. Releasing it here
3107
- // prevents the watcher from retaining another full-vault projection next
3108
- // to the targeted-pull and area-ledger views.
3332
+ // being assembled, but completed watch cycles do not. Area-authoritative
3333
+ // journals are different: their bridge owns the packed aggregate that a
3334
+ // later watcher callback reuses. Dropping that bridge at every poll
3335
+ // makes the next scoped callback synchronously re-pack the whole vault.
3336
+ // Keep it for this runner's lifetime; layout/phase changes still close
3337
+ // it in openAreaMigrationIfAuthoritative(), and final shutdown releases
3338
+ // every bridge below.
3109
3339
  releaseWatcherJournalStores();
3110
- releaseCachedAreaJournalMigrations();
3111
3340
  const exitCode = passExitCode(result);
3112
3341
  if (exitCode === runtime.authRequiredPassExit) {
3113
3342
  // The pass already emitted auth-error. Hand the sentinel to the
@@ -3142,8 +3371,19 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3142
3371
  : "") +
3143
3372
  "; staying alive, retrying next poll\n");
3144
3373
  }
3374
+ else if (exitCode === UNRESOLVED_COMPANY_EXIT) {
3375
+ // HQ-SYNC-COMPANY-ROUTE-UID: routed paths are derived from local
3376
+ // company directory names, which may not be memberships. The runner
3377
+ // has already emitted the resolution error; terminating here would
3378
+ // restart the outpost watcher every 60 seconds for each such route.
3379
+ // Keep the watcher alive while preserving the one-shot non-zero code.
3380
+ process.stderr.write("hq-sync-runner: watch pass skipped — unresolved company target; retrying next poll\n");
3381
+ }
3145
3382
  else if (exitCode !== 0) {
3146
- return exitCode;
3383
+ // A scoped company-resolution or per-pass failure is isolated to that
3384
+ // pass. The error was emitted by the runner; terminating the daemon
3385
+ // recreates the startup blind window and strands newer watcher work.
3386
+ process.stderr.write(`hq-sync-runner: watch pass failed (exit=${exitCode}); staying alive, retrying next poll\n`);
3147
3387
  }
3148
3388
  const completedPostRepairPass = !enqueuedFullChore && (typeof result === "number"
3149
3389
  ? exitCode === 0
@@ -3247,6 +3487,10 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3247
3487
  releaseWatcherJournalStores();
3248
3488
  releaseCachedAreaJournalMigrations();
3249
3489
  await shutdown();
3490
+ // Coordinator shutdown waits for any active pass before the retained
3491
+ // projections are dropped. A shutdown must never evict a store that a
3492
+ // final scoped writer is still refreshing or committing through.
3493
+ disposeScopedJournalStores();
3250
3494
  detachSignal?.();
3251
3495
  }
3252
3496
  }