@indigoai-us/hq-cloud 6.16.64 → 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.
- package/dist/bin/sync-runner-company.d.ts +14 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +42 -8
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +62 -7
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/bin/sync-runner-planning.d.ts +12 -3
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +65 -3
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-planning.test.js +172 -1
- package/dist/bin/sync-runner-planning.test.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +52 -3
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +443 -167
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +19 -2
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +40 -5
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.test.js +47 -0
- package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +64 -6
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +48 -4
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +1577 -169
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts +2 -0
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts.map +1 -0
- package/dist/cli/onepass-pull-planner-regressions.test.js +300 -0
- package/dist/cli/onepass-pull-planner-regressions.test.js.map +1 -0
- package/dist/cli/onepass-pull-planner.test.d.ts +2 -0
- package/dist/cli/onepass-pull-planner.test.d.ts.map +1 -0
- package/dist/cli/onepass-pull-planner.test.js +266 -0
- package/dist/cli/onepass-pull-planner.test.js.map +1 -0
- package/dist/cli/push-scan-exclusions.test.js +4 -4
- package/dist/cli/push-scan-exclusions.test.js.map +1 -1
- package/dist/cli/share.d.ts +31 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +136 -6
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +295 -5
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/streaming-pull-planner.bench.test.d.ts +15 -0
- package/dist/cli/streaming-pull-planner.bench.test.d.ts.map +1 -0
- package/dist/cli/streaming-pull-planner.bench.test.js +273 -0
- package/dist/cli/streaming-pull-planner.bench.test.js.map +1 -0
- package/dist/cli/sync-scope.test.js +8 -0
- package/dist/cli/sync-scope.test.js.map +1 -1
- package/dist/cli/sync.d.ts +12 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +302 -123
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +255 -2
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/hq-root-recovery.d.ts +30 -0
- package/dist/hq-root-recovery.d.ts.map +1 -0
- package/dist/hq-root-recovery.js +131 -0
- package/dist/hq-root-recovery.js.map +1 -0
- package/dist/hq-root-recovery.test.d.ts +2 -0
- package/dist/hq-root-recovery.test.d.ts.map +1 -0
- package/dist/hq-root-recovery.test.js +65 -0
- package/dist/hq-root-recovery.test.js.map +1 -0
- package/dist/journal-row-store.d.ts +40 -5
- package/dist/journal-row-store.d.ts.map +1 -1
- package/dist/journal-row-store.js +240 -21
- package/dist/journal-row-store.js.map +1 -1
- package/dist/journal-row-store.test.js +54 -0
- package/dist/journal-row-store.test.js.map +1 -1
- package/dist/journal-snapshot-v4.d.ts.map +1 -1
- package/dist/journal-snapshot-v4.js +11 -6
- package/dist/journal-snapshot-v4.js.map +1 -1
- package/dist/journal-store.test.js +324 -7
- package/dist/journal-store.test.js.map +1 -1
- package/dist/journal.d.ts +51 -16
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +303 -79
- package/dist/journal.js.map +1 -1
- package/dist/lib/exit-codes.d.ts +15 -0
- package/dist/lib/exit-codes.d.ts.map +1 -1
- package/dist/lib/exit-codes.js +15 -0
- package/dist/lib/exit-codes.js.map +1 -1
- package/dist/personal-vault.d.ts.map +1 -1
- package/dist/personal-vault.js +10 -3
- package/dist/personal-vault.js.map +1 -1
- package/dist/personal-vault.test.js +35 -0
- package/dist/personal-vault.test.js.map +1 -1
- package/dist/remote-pull.d.ts.map +1 -1
- package/dist/remote-pull.js +9 -2
- package/dist/remote-pull.js.map +1 -1
- package/dist/remote-pull.test.js +59 -2
- package/dist/remote-pull.test.js.map +1 -1
- package/dist/s3.d.ts +1 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +20 -2
- package/dist/s3.js.map +1 -1
- package/dist/s3.list-cost.test.d.ts +2 -0
- package/dist/s3.list-cost.test.d.ts.map +1 -0
- package/dist/s3.list-cost.test.js +51 -0
- package/dist/s3.list-cost.test.js.map +1 -0
- package/dist/sync/area-ledger.d.ts +27 -3
- package/dist/sync/area-ledger.d.ts.map +1 -1
- package/dist/sync/area-ledger.js +149 -39
- package/dist/sync/area-ledger.js.map +1 -1
- package/dist/sync/area-ledger.test.js +192 -5
- package/dist/sync/area-ledger.test.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.test.js +29 -0
- package/dist/sync/parallel-sync-coordinator.test.js.map +1 -1
- package/dist/sync/realtime-telemetry.d.ts +1 -1
- package/dist/sync/realtime-telemetry.d.ts.map +1 -1
- package/dist/sync/realtime-telemetry.js +14 -6
- package/dist/sync/realtime-telemetry.js.map +1 -1
- package/dist/sync/realtime-telemetry.test.js +136 -16
- package/dist/sync/realtime-telemetry.test.js.map +1 -1
- package/dist/sync/remote-file-spool.d.ts +16 -0
- package/dist/sync/remote-file-spool.d.ts.map +1 -0
- package/dist/sync/remote-file-spool.js +91 -0
- package/dist/sync/remote-file-spool.js.map +1 -0
- package/dist/sync/remote-file-spool.test.d.ts +2 -0
- package/dist/sync/remote-file-spool.test.d.ts.map +1 -0
- package/dist/sync/remote-file-spool.test.js +77 -0
- package/dist/sync/remote-file-spool.test.js.map +1 -0
- package/dist/sync/state-store.d.ts +6 -0
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +14 -2
- package/dist/sync/state-store.js.map +1 -1
- package/dist/watch-coverage.d.ts +24 -0
- package/dist/watch-coverage.d.ts.map +1 -0
- package/dist/watch-coverage.js +46 -0
- package/dist/watch-coverage.js.map +1 -0
- package/dist/watch-coverage.test.d.ts +2 -0
- package/dist/watch-coverage.test.d.ts.map +1 -0
- package/dist/watch-coverage.test.js +56 -0
- package/dist/watch-coverage.test.js.map +1 -0
- package/dist/watcher.d.ts +31 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +172 -0
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +78 -0
- package/dist/watcher.test.js.map +1 -1
- package/package.json +2 -1
|
@@ -10,11 +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
|
-
import {
|
|
17
|
+
import { recoverHqRootLocalState } from "../hq-root-recovery.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";
|
|
18
19
|
import { isPersonalVaultDeletionExcluded } from "../personal-vault.js";
|
|
19
20
|
import { NoopPushReceiver, } from "../sync/push-receiver.js";
|
|
20
21
|
import { resolveEventSync, startEventSync as defaultStartEventSync, } from "../sync/event-sync.js";
|
|
@@ -30,6 +31,219 @@ import { InlineSyncWorkerAdapter, } from "../sync/sync-work.js";
|
|
|
30
31
|
import { buildRoutePullArgv, buildScopedPushArgv, buildScopedDrainPullArgv, buildTargetedPullArgv, isWatchRouteSelected, routeChangeToTarget, routeKey, } from "./sync-runner-watch-routes.js";
|
|
31
32
|
import { resolveSkipCompanies } from "./sync-runner-planning.js";
|
|
32
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
|
+
}
|
|
33
247
|
const MAX_FULL_RECONCILE_REASONS = 7;
|
|
34
248
|
/**
|
|
35
249
|
* Area storage is AUTO by default and only applies after durable cutover.
|
|
@@ -62,7 +276,7 @@ function emitFullReconcileDecision({ mode, reasons, triggers, pollTick, configur
|
|
|
62
276
|
* is drained in bounded chunks across successive ticks instead of one giant
|
|
63
277
|
* scoped pass. Size is not a full-reconcile signal — unknown scope is
|
|
64
278
|
* (`overflowed` with no route hints, warm-up catch-up failure, first tick,
|
|
65
|
-
* scheduled interval, bare wake). The crossover for a *chunk* is the number
|
|
279
|
+
* scheduled interval, bare wake, vanished HQ-root coverage). The crossover for a *chunk* is the number
|
|
66
280
|
* of `--scope-path` arguments a targeted pass can carry without becoming a
|
|
67
281
|
* de-facto whole-tree walk. `HQ_SYNC_EVENT_BATCH_LIMIT` overrides; 0/invalid
|
|
68
282
|
* = default. Do not raise this to "fix" a large warm-up catch-up: that only
|
|
@@ -509,6 +723,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
509
723
|
const idleHeartbeatIntervalMs = deps.idleHeartbeatIntervalMs ?? WATCH_IDLE_HEARTBEAT_INTERVAL_MS;
|
|
510
724
|
const monotonicNow = deps.monotonicNow ?? (() => performance.now());
|
|
511
725
|
const runPass = deps.runPass ?? runtime.runPassWithOperationLockAlreadyHeld;
|
|
726
|
+
const runPassSupportsCooperativeCheckpoint = deps.runPass === undefined || deps.runPassSupportsCooperativeCheckpoint === true;
|
|
512
727
|
const pollIdx = argv.indexOf("--poll-remote-ms");
|
|
513
728
|
// An explicit --poll-remote-ms pins a fixed interval (menubar/CLI still set
|
|
514
729
|
// one). When it is omitted, the interval is resolved fresh each cycle from
|
|
@@ -519,7 +734,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
519
734
|
const eventPush = argv.includes("--event-push");
|
|
520
735
|
const fullReconcileIntervalMs = resolveFullReconcileMs();
|
|
521
736
|
const companiesMode = argv.includes("--companies");
|
|
522
|
-
const
|
|
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;
|
|
523
742
|
const personalMode = argv.includes("--personal");
|
|
524
743
|
// Keep this at the watcher ingress rather than importing the runner's
|
|
525
744
|
// resolver: sync-runner imports this loop, and a reverse value import would
|
|
@@ -528,6 +747,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
528
747
|
const watchRouteSelection = {
|
|
529
748
|
companies: companiesMode,
|
|
530
749
|
company: companyMode,
|
|
750
|
+
...(companySlug !== undefined ? { companySlug } : {}),
|
|
531
751
|
personal: personalMode,
|
|
532
752
|
skipPersonal: parsed.skipPersonal === true ||
|
|
533
753
|
skipPersonalEnv === "1" ||
|
|
@@ -672,7 +892,27 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
672
892
|
resolveNoOperationLockUsers = resolve;
|
|
673
893
|
});
|
|
674
894
|
};
|
|
675
|
-
const
|
|
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
|
+
};
|
|
676
916
|
// Observe the heap at pass boundaries — a gated checkpoint immediately before
|
|
677
917
|
// admitting a pass and immediately after it returns. A pass that runs a long
|
|
678
918
|
// synchronous stretch starves the interval timer, so these boundary
|
|
@@ -684,9 +924,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
684
924
|
heapCheckpoint?.();
|
|
685
925
|
if (typeof outcome === "number")
|
|
686
926
|
return;
|
|
687
|
-
|
|
688
|
-
rememberAppliedUpsert(applied.relativePath, applied.contentHash);
|
|
689
|
-
}
|
|
927
|
+
recordDurableAppliedUpserts(outcome.result?.appliedUpserts ?? []);
|
|
690
928
|
};
|
|
691
929
|
// Pre-admit boundary observation.
|
|
692
930
|
heapCheckpoint?.();
|
|
@@ -695,13 +933,31 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
695
933
|
// its keyed journal delta and per-object mutation are independent from the
|
|
696
934
|
// slow pass's bulk paths, and the slow pass already owns the root lock.
|
|
697
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
|
+
}
|
|
698
954
|
if (lane === "fast" &&
|
|
699
955
|
processOwnsOperationLock &&
|
|
700
956
|
coordinatorWholeMutationChores === 0) {
|
|
701
957
|
acquireOperationLockUser();
|
|
702
958
|
try {
|
|
703
959
|
prepare?.();
|
|
704
|
-
const outcome = await
|
|
960
|
+
const outcome = await invokePass();
|
|
705
961
|
recordAppliedUpserts(outcome);
|
|
706
962
|
return outcome;
|
|
707
963
|
}
|
|
@@ -721,7 +977,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
721
977
|
acquireOperationLockUser();
|
|
722
978
|
}
|
|
723
979
|
prepare?.();
|
|
724
|
-
const outcome = await
|
|
980
|
+
const outcome = await invokePass();
|
|
725
981
|
recordAppliedUpserts(outcome);
|
|
726
982
|
return outcome;
|
|
727
983
|
}
|
|
@@ -755,7 +1011,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
755
1011
|
}
|
|
756
1012
|
try {
|
|
757
1013
|
prepare?.();
|
|
758
|
-
const outcome = await
|
|
1014
|
+
const outcome = await invokePass();
|
|
759
1015
|
recordAppliedUpserts(outcome);
|
|
760
1016
|
return outcome;
|
|
761
1017
|
}
|
|
@@ -778,7 +1034,18 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
778
1034
|
throw err;
|
|
779
1035
|
}
|
|
780
1036
|
};
|
|
781
|
-
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
|
+
}
|
|
782
1049
|
try {
|
|
783
1050
|
const handle = acquireOperationLock(hqRoot, "sync", {
|
|
784
1051
|
timeoutSec: 0,
|
|
@@ -872,102 +1139,15 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
872
1139
|
heapCheckpoint?.();
|
|
873
1140
|
}
|
|
874
1141
|
};
|
|
875
|
-
let activeSlowPass = null;
|
|
876
|
-
let activeFastPass = null;
|
|
877
|
-
const pendingSlowPasses = [];
|
|
878
|
-
const pendingFastPasses = [];
|
|
879
1142
|
const resolveStoppedQueue = () => {
|
|
880
|
-
|
|
881
|
-
while (queue.length > 0)
|
|
882
|
-
queue.shift()?.resolve(0);
|
|
883
|
-
}
|
|
884
|
-
};
|
|
885
|
-
const drainQueuedPasses = () => {
|
|
886
|
-
if (stopped) {
|
|
887
|
-
resolveStoppedQueue();
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
// A queued slow pass is the bulk-drain progress boundary. Admit it before
|
|
891
|
-
// refilling the realtime queue; otherwise a steady stream of fast work
|
|
892
|
-
// perpetually leapfrogs a receiver's bulk batch and its SQS poll loop
|
|
893
|
-
// cannot issue another receive.
|
|
894
|
-
if (activeSlowPass === null &&
|
|
895
|
-
activeFastPass === null &&
|
|
896
|
-
pendingSlowPasses.length > 0) {
|
|
897
|
-
const slow = pendingSlowPasses.shift();
|
|
898
|
-
if (slow) {
|
|
899
|
-
const current = startGuardedTask(slow.task, "slow");
|
|
900
|
-
void current.then(slow.resolve, slow.reject);
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
if (activeFastPass === null) {
|
|
904
|
-
const fast = pendingFastPasses.shift();
|
|
905
|
-
if (fast) {
|
|
906
|
-
const current = startGuardedTask(fast.task, "fast");
|
|
907
|
-
void current.then(fast.resolve, fast.reject);
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
// A fast pass that acquired the operation lock normally (rather than
|
|
911
|
-
// bypassing an in-process slow holder) still owns the cross-process
|
|
912
|
-
// boundary. Slow work must queue behind it; only fast work is allowed to
|
|
913
|
-
// overlap an already-lock-owning slow pass.
|
|
914
|
-
if (activeSlowPass === null && activeFastPass === null) {
|
|
915
|
-
const slow = pendingSlowPasses.shift();
|
|
916
|
-
if (slow) {
|
|
917
|
-
const current = startGuardedTask(slow.task, "slow");
|
|
918
|
-
void current.then(slow.resolve, slow.reject);
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
};
|
|
922
|
-
const startGuardedTask = (task, lane) => {
|
|
923
|
-
// Claim the lane before invoking `task`: a task can synchronously activate
|
|
924
|
-
// a watcher/scheduler callback before returning its promise. Without this
|
|
925
|
-
// placeholder that re-entrant callback sees an idle lane and starts a
|
|
926
|
-
// second lock contender.
|
|
927
|
-
const placeholder = Promise.resolve(0);
|
|
928
|
-
if (lane === "fast")
|
|
929
|
-
activeFastPass = placeholder;
|
|
930
|
-
else
|
|
931
|
-
activeSlowPass = placeholder;
|
|
932
|
-
let current;
|
|
933
|
-
try {
|
|
934
|
-
current = stopped ? placeholder : task();
|
|
935
|
-
}
|
|
936
|
-
catch (err) {
|
|
937
|
-
current = Promise.reject(err);
|
|
938
|
-
}
|
|
939
|
-
if (lane === "fast")
|
|
940
|
-
activeFastPass = current;
|
|
941
|
-
else
|
|
942
|
-
activeSlowPass = current;
|
|
943
|
-
void current
|
|
944
|
-
.finally(() => {
|
|
945
|
-
if (lane === "fast" && activeFastPass === current) {
|
|
946
|
-
activeFastPass = null;
|
|
947
|
-
drainQueuedPasses();
|
|
948
|
-
}
|
|
949
|
-
else if (lane === "slow" && activeSlowPass === current) {
|
|
950
|
-
activeSlowPass = null;
|
|
951
|
-
drainQueuedPasses();
|
|
952
|
-
}
|
|
953
|
-
})
|
|
954
|
-
.catch(() => undefined);
|
|
955
|
-
return current;
|
|
1143
|
+
passScheduler.stop();
|
|
956
1144
|
};
|
|
957
|
-
const runGuardedTask = (task, lane = "slow") =>
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
return startGuardedTask(task, lane);
|
|
964
|
-
}
|
|
965
|
-
return new Promise((resolve, reject) => {
|
|
966
|
-
queued.push({ task, lane, resolve, reject });
|
|
967
|
-
drainQueuedPasses();
|
|
968
|
-
});
|
|
969
|
-
};
|
|
970
|
-
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);
|
|
971
1151
|
/*
|
|
972
1152
|
* Area storage and the parallel coordinator default to AUTO. AUTO turns
|
|
973
1153
|
* them on after durable personal-vault cutover; explicit off is the kill
|
|
@@ -1159,6 +1339,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1159
1339
|
});
|
|
1160
1340
|
},
|
|
1161
1341
|
onCutOver: () => {
|
|
1342
|
+
releaseScopedJournalStore(PERSONAL_VAULT_JOURNAL_SLUG);
|
|
1162
1343
|
parallelCoordinatorEnabled = refreshCoordinatorEnabled();
|
|
1163
1344
|
if (parallelCoordinatorEnabled && coordinator === null) {
|
|
1164
1345
|
coordinator = createCoordinator();
|
|
@@ -1167,6 +1348,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1167
1348
|
})
|
|
1168
1349
|
: { onTick() { } };
|
|
1169
1350
|
let watcher = null;
|
|
1351
|
+
let rootCoverageStale = false;
|
|
1170
1352
|
let eventPushSurfacesActivated = false;
|
|
1171
1353
|
let watcherUnavailableWarned = false;
|
|
1172
1354
|
let watcherRetryAt = null;
|
|
@@ -1240,11 +1422,36 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1240
1422
|
`watched-paths=${watchedPaths ?? "unknown"}, ` +
|
|
1241
1423
|
`dropped-untracked-deletes=${watcher.untrackedDeletesDropped?.() ?? "unknown"}, ` +
|
|
1242
1424
|
`drained-untracked-deletes=${drainedUntrackedDeletes})\n`);
|
|
1425
|
+
const probe = watcher.probeCoverage?.();
|
|
1426
|
+
if (probe && !probe.ok) {
|
|
1427
|
+
process.stderr.write("hq-sync-runner: WARN event-push watcher coverage stale; " +
|
|
1428
|
+
`reason=${probe.reason} stale-watches=${probe.staleWatches} ` +
|
|
1429
|
+
`watched-paths=${watchedPaths ?? "unknown"}; restoring from vault, not pushing deletes\n`);
|
|
1430
|
+
try {
|
|
1431
|
+
recoverHqRootLocalState(hqRoot);
|
|
1432
|
+
}
|
|
1433
|
+
catch (err) {
|
|
1434
|
+
process.stderr.write(`hq-sync-runner: WARN local HQ recovery failed: ${describeError(err)}\n`);
|
|
1435
|
+
}
|
|
1436
|
+
try {
|
|
1437
|
+
watcher.rebuildCoverage?.();
|
|
1438
|
+
}
|
|
1439
|
+
catch (err) {
|
|
1440
|
+
process.stderr.write(`hq-sync-runner: WARN watcher coverage rebuild failed: ${describeError(err)}\n`);
|
|
1441
|
+
}
|
|
1442
|
+
rootCoverageStale = true;
|
|
1443
|
+
interruptWait();
|
|
1444
|
+
}
|
|
1243
1445
|
};
|
|
1244
1446
|
/** Vanished never-journaled file deletes removed from batches at drain time. */
|
|
1245
1447
|
let drainedUntrackedDeletes = 0;
|
|
1246
1448
|
const pendingWatcherPaths = new Map();
|
|
1247
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;
|
|
1248
1455
|
let pendingWatcherOriginalBatch = null;
|
|
1249
1456
|
// Every coalesced watcher batch gets one durable in-process generation.
|
|
1250
1457
|
// A wake can cause the scheduler to revisit a drained batch, but an event
|
|
@@ -1268,11 +1475,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1268
1475
|
const paths = new Map();
|
|
1269
1476
|
const changes = new Map();
|
|
1270
1477
|
for (const [absolutePath, relativePath] of batch.paths.entries()) {
|
|
1271
|
-
const
|
|
1478
|
+
const change = batch.changes?.get(absolutePath);
|
|
1479
|
+
const route = routeChangeToTarget(relativePath, change?.kind);
|
|
1272
1480
|
if (!route || !isWatchRouteSelected(route, watchRouteSelection))
|
|
1273
1481
|
continue;
|
|
1274
1482
|
paths.set(absolutePath, relativePath);
|
|
1275
|
-
const change = batch.changes?.get(absolutePath);
|
|
1276
1483
|
if (change)
|
|
1277
1484
|
changes.set(absolutePath, change);
|
|
1278
1485
|
}
|
|
@@ -1280,7 +1487,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1280
1487
|
return null;
|
|
1281
1488
|
return { ...batch, paths, changes };
|
|
1282
1489
|
};
|
|
1283
|
-
const addPendingWatcherChange = (changedRelPath, batch) => {
|
|
1490
|
+
const addPendingWatcherChange = (changedRelPath, batch, options) => {
|
|
1284
1491
|
if (batch) {
|
|
1285
1492
|
const selectedBatch = selectWatchBatch(batch);
|
|
1286
1493
|
if (!selectedBatch)
|
|
@@ -1293,20 +1500,30 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1293
1500
|
return false;
|
|
1294
1501
|
}
|
|
1295
1502
|
}
|
|
1296
|
-
pendingWatcherBatchGeneration ??= ++nextWatcherBatchGeneration;
|
|
1297
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;
|
|
1298
1513
|
pendingWatcherOriginalBatch =
|
|
1299
1514
|
pendingWatcherPaths.size === 0 &&
|
|
1300
1515
|
!pendingWatcherBareChange &&
|
|
1301
1516
|
!pendingWatcherOverflowed &&
|
|
1302
|
-
!batch.overflowed
|
|
1517
|
+
!batch.overflowed &&
|
|
1518
|
+
entries.length === batch.paths.size
|
|
1303
1519
|
? batch
|
|
1304
1520
|
: null;
|
|
1305
|
-
for (const [absolutePath, relativePath] of
|
|
1521
|
+
for (const [absolutePath, relativePath] of entries) {
|
|
1306
1522
|
pendingWatcherPaths.set(absolutePath, relativePath);
|
|
1307
1523
|
const change = batch.changes?.get(absolutePath);
|
|
1308
1524
|
if (change)
|
|
1309
1525
|
pendingWatcherChanges.set(absolutePath, change);
|
|
1526
|
+
pendingWatcherChangeSequences.set(absolutePath, ++nextPendingWatcherChangeSequence);
|
|
1310
1527
|
}
|
|
1311
1528
|
if (batch.overflowed) {
|
|
1312
1529
|
pendingWatcherOverflowed = true;
|
|
@@ -1323,6 +1540,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1323
1540
|
}
|
|
1324
1541
|
return true;
|
|
1325
1542
|
}
|
|
1543
|
+
pendingWatcherBatchGeneration ??= ++nextWatcherBatchGeneration;
|
|
1326
1544
|
if (changedRelPath) {
|
|
1327
1545
|
pendingWatcherOriginalBatch = null;
|
|
1328
1546
|
// Codec boundary: `changedRelPath` is a canonical vault key (the
|
|
@@ -1331,6 +1549,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1331
1549
|
// point at the impossible colon path on Windows.
|
|
1332
1550
|
const absolutePath = localPathForVaultKey(hqRoot, changedRelPath);
|
|
1333
1551
|
pendingWatcherPaths.set(absolutePath, changedRelPath);
|
|
1552
|
+
pendingWatcherChangeSequences.set(absolutePath, ++nextPendingWatcherChangeSequence);
|
|
1334
1553
|
// A bare path signal carries no delete intent; drop any change record
|
|
1335
1554
|
// a prior batch left for this path so the stale intent cannot leak
|
|
1336
1555
|
// into the coalesced batch (#227).
|
|
@@ -1372,6 +1591,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1372
1591
|
const generation = pendingWatcherBatchGeneration;
|
|
1373
1592
|
pendingWatcherPaths.clear();
|
|
1374
1593
|
pendingWatcherChanges.clear();
|
|
1594
|
+
pendingWatcherChangeSequences.clear();
|
|
1375
1595
|
pendingWatcherOriginalBatch = null;
|
|
1376
1596
|
pendingWatcherBareChange = false;
|
|
1377
1597
|
pendingWatcherWarmupCatchup = false;
|
|
@@ -1390,6 +1610,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1390
1610
|
pendingWatcherOriginalBatch = null;
|
|
1391
1611
|
for (const [absolutePath, relativePath] of paths.entries()) {
|
|
1392
1612
|
pendingWatcherPaths.set(absolutePath, relativePath);
|
|
1613
|
+
pendingWatcherChangeSequences.set(absolutePath, ++nextPendingWatcherChangeSequence);
|
|
1393
1614
|
}
|
|
1394
1615
|
};
|
|
1395
1616
|
/**
|
|
@@ -1470,8 +1691,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1470
1691
|
}
|
|
1471
1692
|
return null;
|
|
1472
1693
|
};
|
|
1473
|
-
const journalCoordinates = (relativePath) => {
|
|
1474
|
-
const route = routeChangeToTarget(relativePath);
|
|
1694
|
+
const journalCoordinates = (relativePath, kind) => {
|
|
1695
|
+
const route = routeChangeToTarget(relativePath, kind);
|
|
1475
1696
|
if (!route || !isWatchRouteSelected(route, watchRouteSelection))
|
|
1476
1697
|
return null;
|
|
1477
1698
|
if (route.kind === "personal") {
|
|
@@ -1506,7 +1727,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1506
1727
|
declaredSize >= 0) {
|
|
1507
1728
|
return declaredSize;
|
|
1508
1729
|
}
|
|
1509
|
-
const size = journalRows(
|
|
1730
|
+
const size = journalRows(readJournalScoped(coordinates.slug, [coordinates.key]))
|
|
1731
|
+
.get(coordinates.key)?.size;
|
|
1510
1732
|
return typeof size === "number" && Number.isSafeInteger(size) && size >= 0 ? size : null;
|
|
1511
1733
|
};
|
|
1512
1734
|
/**
|
|
@@ -1553,8 +1775,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1553
1775
|
return session.get(coordinates.key) === undefined;
|
|
1554
1776
|
};
|
|
1555
1777
|
const captureLocalDeleteSnapshots = (relativePath, kind) => {
|
|
1556
|
-
const coordinates = journalCoordinates(relativePath);
|
|
1557
|
-
const route = routeChangeToTarget(relativePath);
|
|
1778
|
+
const coordinates = journalCoordinates(relativePath, kind);
|
|
1779
|
+
const route = routeChangeToTarget(relativePath, kind);
|
|
1558
1780
|
if (!coordinates || !route)
|
|
1559
1781
|
return [];
|
|
1560
1782
|
// Desktop workspace Off toggles are authoritative before journal I/O.
|
|
@@ -1675,8 +1897,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1675
1897
|
* it scopes a push or pull for the affected batch.
|
|
1676
1898
|
*/
|
|
1677
1899
|
const captureQueuedLocalDeleteSnapshots = async (relativePath, kind, isCurrent) => {
|
|
1678
|
-
const coordinates = journalCoordinates(relativePath);
|
|
1679
|
-
const route = routeChangeToTarget(relativePath);
|
|
1900
|
+
const coordinates = journalCoordinates(relativePath, kind);
|
|
1901
|
+
const route = routeChangeToTarget(relativePath, kind);
|
|
1680
1902
|
if (!coordinates || !route)
|
|
1681
1903
|
return [];
|
|
1682
1904
|
if (route.kind === "company" &&
|
|
@@ -1770,8 +1992,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1770
1992
|
}
|
|
1771
1993
|
return snapshots;
|
|
1772
1994
|
};
|
|
1773
|
-
const warmupJournalPaths = ()
|
|
1774
|
-
const paths = [];
|
|
1995
|
+
const warmupJournalPaths = function* () {
|
|
1775
1996
|
const journals = listJournals();
|
|
1776
1997
|
const vaultJournalPresent = journals.some(({ slug }) => slug === PERSONAL_VAULT_JOURNAL_SLUG);
|
|
1777
1998
|
for (const { slug, journal } of journals) {
|
|
@@ -1804,10 +2025,9 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1804
2025
|
// ~12.8k of them per runner start after a transcript prune.
|
|
1805
2026
|
if (route.kind === "personal" && isPersonalVaultDeletionExcluded(key))
|
|
1806
2027
|
continue;
|
|
1807
|
-
|
|
2028
|
+
yield route.kind === "personal" ? key : `companies/${route.slug}/${key}`;
|
|
1808
2029
|
}
|
|
1809
2030
|
}
|
|
1810
|
-
return paths;
|
|
1811
2031
|
};
|
|
1812
2032
|
const publishedContentHashLookup = (relativePath) => {
|
|
1813
2033
|
const coordinates = journalCoordinates(relativePath);
|
|
@@ -1820,13 +2040,34 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1820
2040
|
return publishedContentHashFromJournalEntryBeforeSession(entry, serviceStartedAt);
|
|
1821
2041
|
};
|
|
1822
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
|
+
}
|
|
1823
2064
|
const journals = new Map();
|
|
1824
2065
|
const dirty = new Set();
|
|
1825
2066
|
const journalFor = (slug) => {
|
|
1826
2067
|
const loaded = journals.get(slug);
|
|
1827
2068
|
if (loaded)
|
|
1828
2069
|
return loaded;
|
|
1829
|
-
const journal =
|
|
2070
|
+
const journal = readJournalScoped(slug, [...(keysByJournal.get(slug) ?? [])]);
|
|
1830
2071
|
journals.set(slug, journal);
|
|
1831
2072
|
return journal;
|
|
1832
2073
|
};
|
|
@@ -1884,7 +2125,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1884
2125
|
const groupBatchByRoute = (batch) => {
|
|
1885
2126
|
const grouped = new Map();
|
|
1886
2127
|
for (const [absolutePath, relativePath] of batch.paths.entries()) {
|
|
1887
|
-
const
|
|
2128
|
+
const change = batch.changes?.get(absolutePath);
|
|
2129
|
+
const route = routeChangeToTarget(relativePath, change?.kind);
|
|
1888
2130
|
if (!route)
|
|
1889
2131
|
continue;
|
|
1890
2132
|
const key = routeKey(route);
|
|
@@ -1897,7 +2139,6 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1897
2139
|
deleteScopeRoots: [],
|
|
1898
2140
|
};
|
|
1899
2141
|
existing.paths.set(absolutePath, relativePath);
|
|
1900
|
-
const change = batch.changes?.get(absolutePath);
|
|
1901
2142
|
if (change)
|
|
1902
2143
|
existing.changes.set(absolutePath, change);
|
|
1903
2144
|
existing.relPaths = [...new Set([...existing.paths.values()])];
|
|
@@ -1981,7 +2222,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1981
2222
|
}
|
|
1982
2223
|
return false;
|
|
1983
2224
|
};
|
|
1984
|
-
const runScopedDrain = async (batch, watcherBatchGeneration, wakeOnPushFailure = false) => {
|
|
2225
|
+
const runScopedDrain = async (batch, watcherBatchGeneration, wakeOnPushFailure = false, allowFastRealtimeOvertake = false) => {
|
|
1985
2226
|
if (batch)
|
|
1986
2227
|
await resolveDeleteSnapshotCaptures(batch);
|
|
1987
2228
|
const grouped = batch && batch.paths.size > 0 ? groupBatchByRoute(batch) : null;
|
|
@@ -2010,11 +2251,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2010
2251
|
const scopedArgv = buildScopedPushArgv(group.route, admittedPaths, passArgv, group.deleteScopeRoots);
|
|
2011
2252
|
return runGuarded(scopedArgv, admittedChanges.size > 0
|
|
2012
2253
|
? () => prepareWatcherChanges(admittedChanges)
|
|
2013
|
-
: undefined, lane);
|
|
2254
|
+
: undefined, lane, "realtime", allowFastRealtimeOvertake);
|
|
2014
2255
|
})
|
|
2015
2256
|
: { result: await runGuarded(buildScopedPushArgv(group.route, group.relPaths, passArgv, group.deleteScopeRoots), group.changes.size > 0
|
|
2016
2257
|
? () => prepareWatcherChanges(group.changes)
|
|
2017
|
-
: undefined, lane), accepted: true, coordinatorRefusedPaths: [] };
|
|
2258
|
+
: undefined, lane, "realtime", allowFastRealtimeOvertake), accepted: true, coordinatorRefusedPaths: [] };
|
|
2018
2259
|
const result = execution.result;
|
|
2019
2260
|
if (execution.accepted && passExitCode(result) === 0) {
|
|
2020
2261
|
const refusedPaths = new Set(typeof result === "number"
|
|
@@ -2084,8 +2325,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2084
2325
|
? "fast"
|
|
2085
2326
|
: "slow";
|
|
2086
2327
|
const execution = parallelCoordinatorEnabled
|
|
2087
|
-
? await runRealtimePass("watcher", "pull", group.relPaths, lane, (slice) => runGuarded(buildTargetedPullArgv(group.route, passArgv, slice.paths.map((path) => path.canonicalPath)), undefined, lane))
|
|
2088
|
-
: { 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 };
|
|
2089
2330
|
const exit = execution.accepted ? passExitCode(execution.result) : 1;
|
|
2090
2331
|
if (exit !== 0 && worstExit === 0)
|
|
2091
2332
|
worstExit = exit;
|
|
@@ -2290,13 +2531,13 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2290
2531
|
return;
|
|
2291
2532
|
}
|
|
2292
2533
|
if (lanes?.fast &&
|
|
2293
|
-
|
|
2534
|
+
(passScheduler.hasActiveChore || passScheduler.hasActiveSlowRealtime)) {
|
|
2294
2535
|
const generation = ++nextWatcherBatchGeneration;
|
|
2295
2536
|
// Fast dispatch bypasses the pending-batch ingress, so preserve both
|
|
2296
2537
|
// of its contracts here: only selected routes may run, and V2 sees
|
|
2297
2538
|
// the same dirty signal as the compatibility drain.
|
|
2298
2539
|
realtimeState.scheduler?.signal("watcher");
|
|
2299
|
-
void runScopedDrain(lanes.fast, generation, true).then((exit) => {
|
|
2540
|
+
void runScopedDrain(lanes.fast, generation, true, true).then((exit) => {
|
|
2300
2541
|
if (exit === 0)
|
|
2301
2542
|
return;
|
|
2302
2543
|
restorePendingWatcherPaths(lanes.fast.paths, generation);
|
|
@@ -2320,6 +2561,12 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2320
2561
|
interruptWait();
|
|
2321
2562
|
}
|
|
2322
2563
|
});
|
|
2564
|
+
try {
|
|
2565
|
+
recoverHqRootLocalState(hqRoot);
|
|
2566
|
+
}
|
|
2567
|
+
catch (err) {
|
|
2568
|
+
process.stderr.write(`hq-sync-runner: WARN local HQ recovery failed: ${describeError(err)}\n`);
|
|
2569
|
+
}
|
|
2323
2570
|
watcher.start();
|
|
2324
2571
|
const reportWatcherReady = () => {
|
|
2325
2572
|
if (stopped || watcher === null)
|
|
@@ -2345,29 +2592,36 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2345
2592
|
watcherRediscoveryRetries = 0;
|
|
2346
2593
|
process.stderr.write(`hq-sync-runner: event-push watcher active (watched-paths=${watchedPaths ?? "unknown"})\n`);
|
|
2347
2594
|
const startedAt = Date.now();
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
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;
|
|
2363
2622
|
interruptWait();
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
process.stderr.write(`hq-sync-runner: watcher warm-up catch-up failed: ${describeError(err)}\n`);
|
|
2367
|
-
// A scan failure must not silently extend the warm-up blind spot.
|
|
2368
|
-
pendingWatcherWarmupCatchupFailed = true;
|
|
2369
|
-
interruptWait();
|
|
2370
|
-
}
|
|
2623
|
+
}
|
|
2624
|
+
})();
|
|
2371
2625
|
};
|
|
2372
2626
|
if (watcher.onReady)
|
|
2373
2627
|
watcher.onReady(reportWatcherReady);
|
|
@@ -2415,7 +2669,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2415
2669
|
// Submit every route in this lane before awaiting any result. In
|
|
2416
2670
|
// particular, a later route's fast receipt must never wait for an
|
|
2417
2671
|
// earlier route's slow pull to finish.
|
|
2418
|
-
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"))));
|
|
2419
2673
|
for (const execution of completed) {
|
|
2420
2674
|
if (!execution.accepted || passExitCode(execution.result) !== 0) {
|
|
2421
2675
|
throw new Error(`targeted pull failed with exit code ${passExitCode(execution.result)}`);
|
|
@@ -2469,7 +2723,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2469
2723
|
}
|
|
2470
2724
|
for (const { route, paths } of pathsByRoute.values()) {
|
|
2471
2725
|
const targetedArgv = buildTargetedPullArgv(route, passArgv, paths);
|
|
2472
|
-
const result = await runGuarded(targetedArgv, undefined, lane);
|
|
2726
|
+
const result = await runGuarded(targetedArgv, undefined, lane, "realtime");
|
|
2473
2727
|
if (passExitCode(result) !== 0) {
|
|
2474
2728
|
throw new Error(`targeted pull failed with exit code ${passExitCode(result)}`);
|
|
2475
2729
|
}
|
|
@@ -2517,14 +2771,14 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2517
2771
|
const execution = await runRealtimePass("wake", "pull", [], "slow", async () => runGuardedTask(() => runTaskWithLock(async () => {
|
|
2518
2772
|
await drain();
|
|
2519
2773
|
return 0;
|
|
2520
|
-
})));
|
|
2774
|
+
}, "realtime"), "slow", "realtime"));
|
|
2521
2775
|
outcome = execution.accepted ? execution.result : 1;
|
|
2522
2776
|
}
|
|
2523
2777
|
else {
|
|
2524
2778
|
outcome = await runGuardedTask(() => runTaskWithLock(async () => {
|
|
2525
2779
|
await drain();
|
|
2526
2780
|
return 0;
|
|
2527
|
-
}));
|
|
2781
|
+
}, "realtime"), "slow", "realtime");
|
|
2528
2782
|
}
|
|
2529
2783
|
}
|
|
2530
2784
|
catch (err) {
|
|
@@ -2720,8 +2974,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2720
2974
|
targetedJournalSessions: () => targetedJournalSessionCensusSizes().targetedJournalSessions,
|
|
2721
2975
|
targetedJournalRows: () => targetedJournalSessionCensusSizes().targetedJournalRows,
|
|
2722
2976
|
cachedAreaJournalMigrations: () => journalCacheCensusSizes().cachedAreaJournalMigrations,
|
|
2723
|
-
pendingSlowPasses: () =>
|
|
2724
|
-
pendingFastPasses: () =>
|
|
2977
|
+
pendingSlowPasses: () => passScheduler.pendingLaneCount("slow"),
|
|
2978
|
+
pendingFastPasses: () => passScheduler.pendingLaneCount("fast"),
|
|
2725
2979
|
},
|
|
2726
2980
|
eventSyncSizes: () => eventSync?.censusSizes?.(),
|
|
2727
2981
|
watcherSizes: () => watcher?.censusSizes?.(),
|
|
@@ -2909,6 +3163,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2909
3163
|
const watcherWarmupCatchup = pending.warmupCatchup;
|
|
2910
3164
|
const watcherWarmupCatchupFailed = pending.warmupCatchupFailed;
|
|
2911
3165
|
const overflowRoutesUnknown = pending.batch?.overflowed === true && !overflowWithKnownRoutes;
|
|
3166
|
+
const coverageStale = rootCoverageStale;
|
|
3167
|
+
rootCoverageStale = false;
|
|
2912
3168
|
const scheduledInterval = fullReconcileIntervalMs === undefined
|
|
2913
3169
|
? pollTick % FULL_RECONCILE_EVERY_TICKS === 0
|
|
2914
3170
|
: lastFullReconcileAt !== null &&
|
|
@@ -2922,6 +3178,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2922
3178
|
"watcher-warmup-catch-up": watcherWarmupCatchup,
|
|
2923
3179
|
"watcher-warmup-catch-up-failed": watcherWarmupCatchupFailed,
|
|
2924
3180
|
"scheduled-interval": scheduledInterval,
|
|
3181
|
+
"root-coverage-stale": coverageStale,
|
|
2925
3182
|
};
|
|
2926
3183
|
const reasons = Object.entries(triggers)
|
|
2927
3184
|
.filter(([, matched]) => matched)
|
|
@@ -2931,7 +3188,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2931
3188
|
bareWake ||
|
|
2932
3189
|
overflowRoutesUnknown ||
|
|
2933
3190
|
watcherWarmupCatchupFailed ||
|
|
2934
|
-
scheduledInterval
|
|
3191
|
+
scheduledInterval ||
|
|
3192
|
+
coverageStale;
|
|
2935
3193
|
// Size is not unknown-scope. A fully-enumerated over-limit batch still
|
|
2936
3194
|
// has every path listed, so drain it in bounded chunks and keep the
|
|
2937
3195
|
// remainder queued. Skip this when a genuine full-reconcile trigger
|
|
@@ -3071,11 +3329,14 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
3071
3329
|
quarantinedDeleteRoots.delete(root);
|
|
3072
3330
|
}
|
|
3073
3331
|
// Delete snapshot capture needs a fresh keyed view while an episode is
|
|
3074
|
-
// being assembled, but completed watch cycles do not.
|
|
3075
|
-
//
|
|
3076
|
-
//
|
|
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.
|
|
3077
3339
|
releaseWatcherJournalStores();
|
|
3078
|
-
releaseCachedAreaJournalMigrations();
|
|
3079
3340
|
const exitCode = passExitCode(result);
|
|
3080
3341
|
if (exitCode === runtime.authRequiredPassExit) {
|
|
3081
3342
|
// The pass already emitted auth-error. Hand the sentinel to the
|
|
@@ -3110,8 +3371,19 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
3110
3371
|
: "") +
|
|
3111
3372
|
"; staying alive, retrying next poll\n");
|
|
3112
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
|
+
}
|
|
3113
3382
|
else if (exitCode !== 0) {
|
|
3114
|
-
|
|
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`);
|
|
3115
3387
|
}
|
|
3116
3388
|
const completedPostRepairPass = !enqueuedFullChore && (typeof result === "number"
|
|
3117
3389
|
? exitCode === 0
|
|
@@ -3215,6 +3487,10 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
3215
3487
|
releaseWatcherJournalStores();
|
|
3216
3488
|
releaseCachedAreaJournalMigrations();
|
|
3217
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();
|
|
3218
3494
|
detachSignal?.();
|
|
3219
3495
|
}
|
|
3220
3496
|
}
|