@indigoai-us/hq-cloud 6.18.52 → 6.18.54
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-audit.d.ts +26 -0
- package/dist/bin/sync-runner-audit.d.ts.map +1 -0
- package/dist/bin/sync-runner-audit.js +51 -0
- package/dist/bin/sync-runner-audit.js.map +1 -0
- package/dist/bin/sync-runner-company.d.ts +18 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +186 -9
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +107 -5
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +1382 -183
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +63 -11
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +350 -95
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/watcher-catchup-skip-store.d.ts +60 -0
- package/dist/bin/watcher-catchup-skip-store.d.ts.map +1 -0
- package/dist/bin/watcher-catchup-skip-store.js +282 -0
- package/dist/bin/watcher-catchup-skip-store.js.map +1 -0
- package/dist/cli/conflict.d.ts +6 -0
- package/dist/cli/conflict.d.ts.map +1 -1
- package/dist/cli/conflict.js +3 -1
- package/dist/cli/conflict.js.map +1 -1
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +95 -5
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/rescue-core.d.ts +25 -1
- package/dist/cli/rescue-core.d.ts.map +1 -1
- package/dist/cli/rescue-core.js +69 -4
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/share.d.ts +27 -0
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +151 -30
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/sync.d.ts +18 -2
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +90 -25
- package/dist/cli/sync.js.map +1 -1
- package/dist/ignore.d.ts +1 -1
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +2 -2
- package/dist/ignore.js.map +1 -1
- package/dist/journal-row-store.d.ts.map +1 -1
- package/dist/journal-row-store.js +6 -0
- package/dist/journal-row-store.js.map +1 -1
- package/dist/journal-snapshot-v4.d.ts +8 -0
- package/dist/journal-snapshot-v4.d.ts.map +1 -1
- package/dist/journal-snapshot-v4.js +208 -17
- package/dist/journal-snapshot-v4.js.map +1 -1
- package/dist/journal.d.ts +18 -2
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +248 -17
- package/dist/journal.js.map +1 -1
- package/dist/lib/cloud-authoritative.d.ts +9 -2
- package/dist/lib/cloud-authoritative.d.ts.map +1 -1
- package/dist/lib/cloud-authoritative.js +34 -2
- package/dist/lib/cloud-authoritative.js.map +1 -1
- package/dist/lib/exit-codes.d.ts +2 -0
- package/dist/lib/exit-codes.d.ts.map +1 -1
- package/dist/lib/exit-codes.js +2 -0
- package/dist/lib/exit-codes.js.map +1 -1
- package/dist/personal-vault-exclusions.d.ts +2 -2
- package/dist/personal-vault-exclusions.d.ts.map +1 -1
- package/dist/personal-vault-exclusions.js +5 -3
- package/dist/personal-vault-exclusions.js.map +1 -1
- package/dist/personal-vault.d.ts +7 -0
- package/dist/personal-vault.d.ts.map +1 -1
- package/dist/personal-vault.js +10 -0
- package/dist/personal-vault.js.map +1 -1
- package/dist/qmd-runner-admission.d.ts +22 -0
- package/dist/qmd-runner-admission.d.ts.map +1 -0
- package/dist/qmd-runner-admission.js +107 -0
- package/dist/qmd-runner-admission.js.map +1 -0
- package/dist/s3.d.ts +2 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +2 -0
- package/dist/s3.js.map +1 -1
- package/dist/symlink-target-policy.d.ts +11 -0
- package/dist/symlink-target-policy.d.ts.map +1 -1
- package/dist/symlink-target-policy.js +110 -0
- package/dist/symlink-target-policy.js.map +1 -1
- package/dist/sync/area-ledger-migration.d.ts +3 -1
- package/dist/sync/area-ledger-migration.d.ts.map +1 -1
- package/dist/sync/area-ledger-migration.js +3 -0
- package/dist/sync/area-ledger-migration.js.map +1 -1
- package/dist/sync/area-ledger.d.ts +3 -0
- package/dist/sync/area-ledger.d.ts.map +1 -1
- package/dist/sync/area-ledger.js +29 -6
- package/dist/sync/area-ledger.js.map +1 -1
- package/dist/sync/mutation-session.d.ts.map +1 -1
- package/dist/sync/mutation-session.js +4 -0
- package/dist/sync/mutation-session.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.d.ts +29 -0
- package/dist/sync/parallel-sync-coordinator.d.ts.map +1 -1
- package/dist/sync/parallel-sync-coordinator.js +229 -16
- package/dist/sync/parallel-sync-coordinator.js.map +1 -1
- package/dist/sync/push-receiver.d.ts +5 -0
- package/dist/sync/push-receiver.d.ts.map +1 -1
- package/dist/sync/push-receiver.js +32 -10
- package/dist/sync/push-receiver.js.map +1 -1
- package/dist/sync/state-store.d.ts +7 -0
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +113 -6
- package/dist/sync/state-store.js.map +1 -1
- package/dist/watch-owner-lease.d.ts +4 -0
- package/dist/watch-owner-lease.d.ts.map +1 -1
- package/dist/watch-owner-lease.js +40 -6
- package/dist/watch-owner-lease.js.map +1 -1
- package/dist/watcher.d.ts +16 -3
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +114 -21
- package/dist/watcher.js.map +1 -1
- package/package.json +4 -3
package/dist/bin/sync-runner.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* fanout. Combined with HQ_SYNC_SKIP_PERSONAL env
|
|
22
22
|
* (either truthy disables personal sync). No-op
|
|
23
23
|
* outside --companies mode.
|
|
24
|
+
* --qmd-full-refresh Bypass the post-sync QMD minimum interval.
|
|
24
25
|
* --json Ignored — ndjson on stdout is the default and
|
|
25
26
|
* only output mode. Accepted for symmetry with the
|
|
26
27
|
* AppBar's argv in case someone passes it.
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
import * as os from "os";
|
|
69
70
|
import * as path from "path";
|
|
70
71
|
import * as fs from "fs";
|
|
72
|
+
import { localPathForVaultKey } from "../local-path-codec.js";
|
|
71
73
|
import { randomUUID } from "node:crypto";
|
|
72
74
|
import { monitorEventLoopDelay, performance } from "node:perf_hooks";
|
|
73
75
|
import { fileURLToPath } from "url";
|
|
@@ -79,6 +81,7 @@ import { PERSONAL_VAULT_EXCLUDED_TOP_LEVEL, computePersonalVaultPaths, } from ".
|
|
|
79
81
|
import { sync as defaultSync } from "../cli/sync.js";
|
|
80
82
|
import { releaseHeldOperationLocks, suppressOperationLockSignalReraise, } from "../operation-lock.js";
|
|
81
83
|
import { share as defaultShare } from "../cli/share.js";
|
|
84
|
+
import { WatcherCatchupSkipStore, watcherCatchupSkipStorePath, } from "./watcher-catchup-skip-store.js";
|
|
82
85
|
import { ensureCompanyVaultTransport } from "../company-vault-transport.js";
|
|
83
86
|
import { HQ_CLOUD_VERSION } from "../version.js";
|
|
84
87
|
import { collectAndSendTelemetry } from "../telemetry.js";
|
|
@@ -87,6 +90,7 @@ import { collectAndSendOutcomeTelemetry, OUTCOME_STORY_DONE_STATE_FLAG, } from "
|
|
|
87
90
|
import { resolveOntologyEntityPush } from "../lib/ontology-entity-push.js";
|
|
88
91
|
import { emitCloudTelemetry } from "../telemetry-events.js";
|
|
89
92
|
import { reindexAfterSync } from "../qmd-reindex.js";
|
|
93
|
+
import { acquireQmdBackgroundLock, isQmdBackgroundLockHeld, qmdRunnerLastFinishedAt, qmdRunnerShouldSkip, recordQmdRunnerFinished, } from "../qmd-runner-admission.js";
|
|
90
94
|
import { reindexAsync as defaultReindexAsync } from "../cli/reindex-async.js";
|
|
91
95
|
/**
|
|
92
96
|
* The runner reindexes after nearly every pass, and each reindex rewrites every
|
|
@@ -101,11 +105,12 @@ import { createPostSyncFlagEvaluator, POST_SYNC_FLAG_KEYS, } from "./sync-runner
|
|
|
101
105
|
import { runPostSyncManifestUploads, } from "./sync-runner-manifest.js";
|
|
102
106
|
import { getOrCreateMachineId } from "../lib/machine-id.js";
|
|
103
107
|
import { isTransientNetworkError, TRANSIENT_NETWORK_EXIT, } from "../lib/net-errors.js";
|
|
104
|
-
import { PARTIAL_SYNC_EXIT, UNRESOLVED_COMPANY_EXIT, WATCH_RUNNER_ALREADY_OWNED_EXIT, WATCH_OWNER_LEASE_LOST_EXIT, } from "../lib/exit-codes.js";
|
|
108
|
+
import { PARTIAL_SYNC_EXIT, UNRESOLVED_COMPANY_EXIT, PREFLIGHT_HALT_EXIT, WATCH_RUNNER_ALREADY_OWNED_EXIT, WATCH_OWNER_LEASE_LOST_EXIT, } from "../lib/exit-codes.js";
|
|
109
|
+
import { SyncRunnerHashAudit } from "./sync-runner-audit.js";
|
|
105
110
|
import { acquireWatchOwnerLease } from "../watch-owner-lease.js";
|
|
106
111
|
import { startWatchParentExitMonitor } from "../watch-parent.js";
|
|
107
112
|
import { describeError } from "../lib/describe-error.js";
|
|
108
|
-
import { migratePersonalVaultJournal, PERSONAL_VAULT_JOURNAL_SLUG, getStateDir, repairJournalStateIfNeeded as defaultRepairJournalStateIfNeeded, } from "../journal.js";
|
|
113
|
+
import { setAsyncJournalCompactionForWatchRunner, migratePersonalVaultJournal, PERSONAL_VAULT_JOURNAL_SLUG, getStateDir, repairJournalStateIfNeeded as defaultRepairJournalStateIfNeeded, } from "../journal.js";
|
|
109
114
|
import { StateStoreLockError } from "../sync/state-store.js";
|
|
110
115
|
import { resolveParallelSyncRollout } from "../sync/feature-flags.js";
|
|
111
116
|
import { createRunnerEmitter, emitUncaughtRunnerError, notifyRunnerEmitObserver, } from "./sync-runner-events.js";
|
|
@@ -460,6 +465,9 @@ export function parseArgs(argv) {
|
|
|
460
465
|
let scopeContentHashes;
|
|
461
466
|
let eventPush = false;
|
|
462
467
|
let lockTimeoutSec;
|
|
468
|
+
let qmdFullRefresh = false;
|
|
469
|
+
let auditHashOut;
|
|
470
|
+
const preflightRaw = [];
|
|
463
471
|
for (let i = 0; i < argv.length; i++) {
|
|
464
472
|
const arg = argv[i];
|
|
465
473
|
switch (arg) {
|
|
@@ -562,6 +570,9 @@ export function parseArgs(argv) {
|
|
|
562
570
|
// @getindigo.ai identities for the first release.
|
|
563
571
|
eventPush = true;
|
|
564
572
|
break;
|
|
573
|
+
case "--qmd-full-refresh":
|
|
574
|
+
qmdFullRefresh = true;
|
|
575
|
+
break;
|
|
565
576
|
case "--scope-path": {
|
|
566
577
|
const val = argv[++i];
|
|
567
578
|
if (!val)
|
|
@@ -634,6 +645,24 @@ export function parseArgs(argv) {
|
|
|
634
645
|
lockTimeoutSec = n;
|
|
635
646
|
break;
|
|
636
647
|
}
|
|
648
|
+
case "--preflight-max-remote-deletes":
|
|
649
|
+
case "--preflight-max-local-deletes":
|
|
650
|
+
case "--preflight-max-pushes":
|
|
651
|
+
case "--preflight-max-pulls": {
|
|
652
|
+
const val = argv[++i];
|
|
653
|
+
if (val === undefined || !/^\d+$/.test(val) || !Number.isSafeInteger(Number(val))) {
|
|
654
|
+
return { error: `${arg} requires a non-negative safe integer` };
|
|
655
|
+
}
|
|
656
|
+
preflightRaw.push(`${arg.slice("--preflight-max-".length)}=${val}`);
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
case "--audit-hash-out": {
|
|
660
|
+
const filename = argv[++i];
|
|
661
|
+
if (!filename)
|
|
662
|
+
return { error: "--audit-hash-out requires a file path" };
|
|
663
|
+
auditHashOut = filename;
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
637
666
|
default:
|
|
638
667
|
return { error: `Unknown argument: ${arg}` };
|
|
639
668
|
}
|
|
@@ -651,6 +680,39 @@ export function parseArgs(argv) {
|
|
|
651
680
|
error: "--personal and --skip-personal are contradictory",
|
|
652
681
|
};
|
|
653
682
|
}
|
|
683
|
+
const preflightKeys = [
|
|
684
|
+
"remote-deletes",
|
|
685
|
+
"local-deletes",
|
|
686
|
+
"pushes",
|
|
687
|
+
"pulls",
|
|
688
|
+
];
|
|
689
|
+
let preflightLimits;
|
|
690
|
+
if (preflightRaw.length > 0) {
|
|
691
|
+
const parsedLimits = Object.fromEntries(preflightRaw.map((entry) => {
|
|
692
|
+
const separator = entry.indexOf("=");
|
|
693
|
+
const key = entry.slice(0, separator);
|
|
694
|
+
const value = entry.slice(separator + 1);
|
|
695
|
+
return [key, Number(value)];
|
|
696
|
+
}));
|
|
697
|
+
if (preflightRaw.length !== 4 ||
|
|
698
|
+
preflightKeys.some((key) => !Object.hasOwn(parsedLimits, key))) {
|
|
699
|
+
return { error: "all four --preflight-max-* limits are required together" };
|
|
700
|
+
}
|
|
701
|
+
if (!company || companies || personal || watch || direction !== "both" || scopePaths.length > 0) {
|
|
702
|
+
return { error: "preflight limits require an unscoped, company-scoped, both-direction one-shot" };
|
|
703
|
+
}
|
|
704
|
+
if (!auditHashOut)
|
|
705
|
+
return { error: "preflight limits require --audit-hash-out" };
|
|
706
|
+
preflightLimits = {
|
|
707
|
+
maxRemoteDeletes: parsedLimits["remote-deletes"],
|
|
708
|
+
maxLocalDeletes: parsedLimits["local-deletes"],
|
|
709
|
+
maxPushes: parsedLimits.pushes,
|
|
710
|
+
maxPulls: parsedLimits.pulls,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
if (auditHashOut && (!company || companies || personal || watch || direction !== "both" || scopePaths.length > 0)) {
|
|
714
|
+
return { error: "--audit-hash-out requires an unscoped, company-scoped, both-direction one-shot" };
|
|
715
|
+
}
|
|
654
716
|
if (!companies && !company && !personal) {
|
|
655
717
|
return { error: "Pass --companies, --company <slug>, or --personal" };
|
|
656
718
|
}
|
|
@@ -697,6 +759,9 @@ export function parseArgs(argv) {
|
|
|
697
759
|
deleteScopePaths,
|
|
698
760
|
eventPush,
|
|
699
761
|
lockTimeoutSec,
|
|
762
|
+
...(qmdFullRefresh ? { qmdFullRefresh: true } : {}),
|
|
763
|
+
...(preflightLimits ? { preflightLimits } : {}),
|
|
764
|
+
...(auditHashOut ? { auditHashOut } : {}),
|
|
700
765
|
};
|
|
701
766
|
}
|
|
702
767
|
// ---------------------------------------------------------------------------
|
|
@@ -987,6 +1052,7 @@ export async function runRunner(argv, deps = {}) {
|
|
|
987
1052
|
stderr.write(`hq-sync-runner: ${parsed.error}\n`);
|
|
988
1053
|
return 1;
|
|
989
1054
|
}
|
|
1055
|
+
setAsyncJournalCompactionForWatchRunner(parsed.watch);
|
|
990
1056
|
// A watch-loop pass also records its end-of-pass events for a host app that
|
|
991
1057
|
// does not read this stdout (the desktop app when `hq daemon` runs sync).
|
|
992
1058
|
const printEvent = createRunnerEmitter({ stdout, stderr });
|
|
@@ -1286,16 +1352,17 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1286
1352
|
for (const target of plan) {
|
|
1287
1353
|
const journalSlug = target.journalSlug ?? target.slug;
|
|
1288
1354
|
if (!journalTargets.has(journalSlug))
|
|
1289
|
-
journalTargets.set(journalSlug, target.slug);
|
|
1355
|
+
journalTargets.set(journalSlug, { company: target.slug, companyUid: target.uid });
|
|
1290
1356
|
}
|
|
1291
1357
|
// The one-time personal-vault seed below reads the legacy `personal` shard
|
|
1292
1358
|
// even when no real company with that slug appears in the fanout plan. Give
|
|
1293
1359
|
// that source the same bounded recovery before migration can open it. A
|
|
1294
1360
|
// missing shard is a metadata-only no-op.
|
|
1295
1361
|
if (!journalTargets.has("personal"))
|
|
1296
|
-
journalTargets.set("personal", "personal");
|
|
1362
|
+
journalTargets.set("personal", { company: "personal", companyUid: "" });
|
|
1297
1363
|
const skippedJournalShards = new Set();
|
|
1298
|
-
for (const [journalSlug,
|
|
1364
|
+
for (const [journalSlug, targetIdentity] of journalTargets) {
|
|
1365
|
+
const { company, companyUid } = targetIdentity;
|
|
1299
1366
|
// Area-authoritative layouts rotate and repair through the watch loop's
|
|
1300
1367
|
// bounded chore cursor. Do not put every area behind this pass's startup
|
|
1301
1368
|
// preflight: a realtime scoped pass must be able to start before a
|
|
@@ -1306,6 +1373,7 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1306
1373
|
}
|
|
1307
1374
|
try {
|
|
1308
1375
|
repairJournalStateIfNeeded(journalSlug, {
|
|
1376
|
+
...(companyUid ? { companyUid } : {}),
|
|
1309
1377
|
onProgress: ({ bytesRead, totalBytes }) => {
|
|
1310
1378
|
emit({
|
|
1311
1379
|
type: "maintenance-progress",
|
|
@@ -1333,8 +1401,12 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1333
1401
|
}
|
|
1334
1402
|
emit({
|
|
1335
1403
|
type: "error",
|
|
1336
|
-
|
|
1337
|
-
|
|
1404
|
+
...(companyUid ? { companyUid } : {}),
|
|
1405
|
+
errorClass: err instanceof Error && err.message.includes("HQSNAP4")
|
|
1406
|
+
? "HQSNAP4EncodeError"
|
|
1407
|
+
: "JournalCompactionError",
|
|
1408
|
+
message: "journal recovery failed",
|
|
1409
|
+
path: "(journal)",
|
|
1338
1410
|
});
|
|
1339
1411
|
emitRunnerTerminalTelemetry({ status: "failed" });
|
|
1340
1412
|
return 1;
|
|
@@ -1345,7 +1417,9 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1345
1417
|
// does NOT trigger a mass re-upload of the HQ overlay on first run. No-op
|
|
1346
1418
|
// when the new journal already exists or the legacy one is absent, and
|
|
1347
1419
|
// harmless even when the personal-vault slot is not in the plan.
|
|
1348
|
-
if (!
|
|
1420
|
+
if (!parsed.preflightLimits &&
|
|
1421
|
+
!skippedJournalShards.has("personal") &&
|
|
1422
|
+
!skippedJournalShards.has(PERSONAL_VAULT_JOURNAL_SLUG)) {
|
|
1349
1423
|
(deps.migratePersonalVaultJournal ?? migratePersonalVaultJournal)();
|
|
1350
1424
|
}
|
|
1351
1425
|
// The journal preflight and personal-vault seed are the point at which an
|
|
@@ -1360,6 +1434,16 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1360
1434
|
// ---- fanout -----------------------------------------------------------
|
|
1361
1435
|
const syncFn = deps.sync ?? defaultSync;
|
|
1362
1436
|
const shareFn = deps.share ?? defaultShare;
|
|
1437
|
+
let audit;
|
|
1438
|
+
if (parsed.auditHashOut) {
|
|
1439
|
+
try {
|
|
1440
|
+
audit = new SyncRunnerHashAudit(parsed.auditHashOut);
|
|
1441
|
+
}
|
|
1442
|
+
catch {
|
|
1443
|
+
emit({ type: "error", path: "(audit)", message: "could not create hash audit output" });
|
|
1444
|
+
return 1;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1363
1447
|
let fanout;
|
|
1364
1448
|
try {
|
|
1365
1449
|
const heartbeatCompany = plan[0]?.slug ?? "(fanout)";
|
|
@@ -1403,6 +1487,9 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1403
1487
|
fanout = await executeCompanyFanout({
|
|
1404
1488
|
plan,
|
|
1405
1489
|
direction: parsed.direction,
|
|
1490
|
+
...(parsed.preflightLimits ? { preflightLimits: parsed.preflightLimits } : {}),
|
|
1491
|
+
grantInventoryComplete: parsed.companies,
|
|
1492
|
+
...(audit ? { audit } : {}),
|
|
1406
1493
|
hqRoot: parsed.hqRoot,
|
|
1407
1494
|
onConflict: parsed.onConflict,
|
|
1408
1495
|
client,
|
|
@@ -1416,6 +1503,16 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1416
1503
|
: {}),
|
|
1417
1504
|
syncFn,
|
|
1418
1505
|
shareFn,
|
|
1506
|
+
...(deps.onCatchupSkip ? { onCatchupSkip: deps.onCatchupSkip } : {}),
|
|
1507
|
+
...(deps.onCatchupGrantCompanies
|
|
1508
|
+
? { onCatchupGrantCompanies: deps.onCatchupGrantCompanies }
|
|
1509
|
+
: {}),
|
|
1510
|
+
...(deps.onCatchupGrantVersion
|
|
1511
|
+
? { onCatchupGrantVersion: deps.onCatchupGrantVersion }
|
|
1512
|
+
: {}),
|
|
1513
|
+
...(deps.onCatchupOntologyPush
|
|
1514
|
+
? { onCatchupOntologyPush: deps.onCatchupOntologyPush }
|
|
1515
|
+
: {}),
|
|
1419
1516
|
resolveDeletePolicy,
|
|
1420
1517
|
emit,
|
|
1421
1518
|
...(emitFanoutHeartbeat ? { onHeartbeat: emitFanoutHeartbeat } : {}),
|
|
@@ -1443,6 +1540,7 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1443
1540
|
finally {
|
|
1444
1541
|
if (heartbeatTimer)
|
|
1445
1542
|
clearInterval(heartbeatTimer);
|
|
1543
|
+
audit?.close();
|
|
1446
1544
|
}
|
|
1447
1545
|
}
|
|
1448
1546
|
catch (err) {
|
|
@@ -1468,6 +1566,8 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1468
1566
|
}
|
|
1469
1567
|
throw err;
|
|
1470
1568
|
}
|
|
1569
|
+
if (fanout.preflightHalted)
|
|
1570
|
+
return PREFLIGHT_HALT_EXIT;
|
|
1471
1571
|
const { errors, transientCompanies, allConflicts, meaningfulOutcomeCount } = fanout;
|
|
1472
1572
|
const rollup = rollupAllComplete(plan, fanout.stateByCompany);
|
|
1473
1573
|
deps.onPassResult?.({
|
|
@@ -1689,82 +1789,131 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1689
1789
|
const qmdChangedPaths = rollup.changedPaths.filter(isQmdIndexableContentPath);
|
|
1690
1790
|
if (qmdChangedPaths.length > 0 &&
|
|
1691
1791
|
postSyncFlags.isEnabled(POST_SYNC_FLAG_KEYS.qmdReindex)) {
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
err,
|
|
1708
|
-
context: { hqRoot: parsed.hqRoot, ...(context ?? {}) },
|
|
1709
|
-
}),
|
|
1710
|
-
});
|
|
1711
|
-
const completedQmdResult = await qmdResult;
|
|
1712
|
-
void emitCloudTelemetry(client, {
|
|
1713
|
-
eventName: "qmd_reindex_run",
|
|
1714
|
-
source: "sync-runner",
|
|
1715
|
-
sessionId: telemetrySessionId,
|
|
1716
|
-
properties: {
|
|
1717
|
-
result: "completed",
|
|
1718
|
-
changedCount: qmdChangedPaths.length,
|
|
1719
|
-
qmdState: completedQmdResult.qmdAvailable ? "available" : "unavailable",
|
|
1720
|
-
collectionsAddedCount: completedQmdResult.collectionsAdded.length,
|
|
1721
|
-
pathDriftDetectedCount: completedQmdResult.pathDriftDetected?.length ?? 0,
|
|
1722
|
-
collectionsRepairedCount: completedQmdResult.collectionsRepaired?.length ?? 0,
|
|
1723
|
-
updateState: completedQmdResult.updated ? "updated" : "unchanged",
|
|
1724
|
-
embedState: completedQmdResult.embedded ? "embedded" : "not_embedded",
|
|
1725
|
-
dirtyState: completedQmdResult.pendingDirty ? "pending_dirty" : "clean",
|
|
1726
|
-
lockBusy: completedQmdResult.lockBusy === true ? 1 : 0,
|
|
1727
|
-
timedOut: completedQmdResult.timedOut === true ? 1 : 0,
|
|
1728
|
-
corruptionQuarantined: completedQmdResult.corruptionQuarantined === true ? 1 : 0,
|
|
1729
|
-
corruptionQuarantineFailed: completedQmdResult.corruptionQuarantineFailed === true ? 1 : 0,
|
|
1730
|
-
},
|
|
1731
|
-
}, {
|
|
1732
|
-
claims,
|
|
1733
|
-
log: (message) => reportDiagnostic({
|
|
1734
|
-
component: "telemetry-events",
|
|
1735
|
-
event: "runner.telemetry_events.nonfatal",
|
|
1736
|
-
message,
|
|
1737
|
-
context: { source: "sync-runner" },
|
|
1738
|
-
}),
|
|
1739
|
-
});
|
|
1740
|
-
}
|
|
1741
|
-
catch (err) {
|
|
1792
|
+
const qmdAdmission = deps.qmdAdmission;
|
|
1793
|
+
const qmdNow = qmdAdmission?.now ?? Date.now;
|
|
1794
|
+
const qmdPlatform = qmdAdmission?.platform ?? process.platform;
|
|
1795
|
+
const qmdEnv = qmdAdmission?.env ?? process.env;
|
|
1796
|
+
const qmdHome = qmdAdmission?.home ?? qmdEnv.HOME ?? os.homedir();
|
|
1797
|
+
const qmdRunnerFinishedByRoot = deps.qmdRunnerFinishedByRoot ?? (deps.qmdRunnerFinishedByRoot = new Map());
|
|
1798
|
+
const qmdLockHeld = isQmdBackgroundLockHeld(qmdHome, { nowMs: qmdNow() });
|
|
1799
|
+
if (qmdRunnerShouldSkip({
|
|
1800
|
+
platform: qmdPlatform,
|
|
1801
|
+
env: qmdEnv,
|
|
1802
|
+
nowMs: qmdNow(),
|
|
1803
|
+
lastFinishedAt: qmdRunnerLastFinishedAt(parsed.hqRoot, qmdRunnerFinishedByRoot),
|
|
1804
|
+
lockHeld: qmdLockHeld,
|
|
1805
|
+
fullRefresh: parsed.qmdFullRefresh,
|
|
1806
|
+
})) {
|
|
1742
1807
|
reportDiagnostic({
|
|
1743
1808
|
component: "qmd-reindex",
|
|
1744
|
-
event: "runner.qmd_reindex.
|
|
1745
|
-
message:
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
// Defensive: reindexAfterSync already swallows internally.
|
|
1750
|
-
void emitCloudTelemetry(client, {
|
|
1751
|
-
eventName: "qmd_reindex_run",
|
|
1752
|
-
source: "sync-runner",
|
|
1753
|
-
sessionId: telemetrySessionId,
|
|
1754
|
-
properties: {
|
|
1755
|
-
result: "threw",
|
|
1756
|
-
changedCount: qmdChangedPaths.length,
|
|
1757
|
-
},
|
|
1758
|
-
}, {
|
|
1759
|
-
claims,
|
|
1760
|
-
log: (message) => reportDiagnostic({
|
|
1761
|
-
component: "telemetry-events",
|
|
1762
|
-
event: "runner.telemetry_events.nonfatal",
|
|
1763
|
-
message,
|
|
1764
|
-
context: { source: "sync-runner" },
|
|
1765
|
-
}),
|
|
1809
|
+
event: "runner.qmd_reindex.skipped_admission",
|
|
1810
|
+
message: qmdLockHeld
|
|
1811
|
+
? "post-sync qmd reindex skipped because the hq-cli background lock is held"
|
|
1812
|
+
: "post-sync qmd reindex skipped because the minimum interval has not elapsed",
|
|
1813
|
+
context: { hqRoot: parsed.hqRoot, lockBusy: qmdLockHeld },
|
|
1766
1814
|
});
|
|
1767
1815
|
}
|
|
1816
|
+
else {
|
|
1817
|
+
try {
|
|
1818
|
+
const releaseSharedLock = acquireQmdBackgroundLock(qmdHome, qmdNow());
|
|
1819
|
+
if (!releaseSharedLock) {
|
|
1820
|
+
reportDiagnostic({
|
|
1821
|
+
component: "qmd-reindex",
|
|
1822
|
+
event: "runner.qmd_reindex.skipped_admission",
|
|
1823
|
+
message: "post-sync qmd reindex skipped because the hq-cli background lock could not be acquired",
|
|
1824
|
+
context: { hqRoot: parsed.hqRoot, lockBusy: true },
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
else {
|
|
1828
|
+
let completedQmdResult;
|
|
1829
|
+
try {
|
|
1830
|
+
const qmdResult = (deps.qmdReindex ?? reindexAfterSync)(parsed.hqRoot, {
|
|
1831
|
+
changedPaths: qmdChangedPaths,
|
|
1832
|
+
forceCollectionRefresh: true,
|
|
1833
|
+
signal: deps.qmdSignal,
|
|
1834
|
+
...(deps.qmdHeartbeat
|
|
1835
|
+
? {
|
|
1836
|
+
onHeartbeat: deps.qmdHeartbeat,
|
|
1837
|
+
heartbeatIntervalMs: deps.qmdHeartbeatIntervalMs,
|
|
1838
|
+
}
|
|
1839
|
+
: {}),
|
|
1840
|
+
log: ({ event, message, err, context }) => reportDiagnostic({
|
|
1841
|
+
component: "qmd-reindex",
|
|
1842
|
+
event,
|
|
1843
|
+
message,
|
|
1844
|
+
err,
|
|
1845
|
+
context: { hqRoot: parsed.hqRoot, ...(context ?? {}) },
|
|
1846
|
+
}),
|
|
1847
|
+
});
|
|
1848
|
+
completedQmdResult = await qmdResult;
|
|
1849
|
+
}
|
|
1850
|
+
finally {
|
|
1851
|
+
releaseSharedLock();
|
|
1852
|
+
}
|
|
1853
|
+
if (completedQmdResult.qmdAvailable &&
|
|
1854
|
+
completedQmdResult.updated &&
|
|
1855
|
+
completedQmdResult.lockBusy !== true &&
|
|
1856
|
+
completedQmdResult.timedOut !== true) {
|
|
1857
|
+
recordQmdRunnerFinished(parsed.hqRoot, qmdNow(), qmdRunnerFinishedByRoot);
|
|
1858
|
+
}
|
|
1859
|
+
void emitCloudTelemetry(client, {
|
|
1860
|
+
eventName: "qmd_reindex_run",
|
|
1861
|
+
source: "sync-runner",
|
|
1862
|
+
sessionId: telemetrySessionId,
|
|
1863
|
+
properties: {
|
|
1864
|
+
result: "completed",
|
|
1865
|
+
changedCount: qmdChangedPaths.length,
|
|
1866
|
+
qmdState: completedQmdResult.qmdAvailable ? "available" : "unavailable",
|
|
1867
|
+
collectionsAddedCount: completedQmdResult.collectionsAdded.length,
|
|
1868
|
+
pathDriftDetectedCount: completedQmdResult.pathDriftDetected?.length ?? 0,
|
|
1869
|
+
collectionsRepairedCount: completedQmdResult.collectionsRepaired?.length ?? 0,
|
|
1870
|
+
updateState: completedQmdResult.updated ? "updated" : "unchanged",
|
|
1871
|
+
embedState: completedQmdResult.embedded ? "embedded" : "not_embedded",
|
|
1872
|
+
dirtyState: completedQmdResult.pendingDirty ? "pending_dirty" : "clean",
|
|
1873
|
+
lockBusy: completedQmdResult.lockBusy === true ? 1 : 0,
|
|
1874
|
+
timedOut: completedQmdResult.timedOut === true ? 1 : 0,
|
|
1875
|
+
corruptionQuarantined: completedQmdResult.corruptionQuarantined === true ? 1 : 0,
|
|
1876
|
+
corruptionQuarantineFailed: completedQmdResult.corruptionQuarantineFailed === true ? 1 : 0,
|
|
1877
|
+
},
|
|
1878
|
+
}, {
|
|
1879
|
+
claims,
|
|
1880
|
+
log: (message) => reportDiagnostic({
|
|
1881
|
+
component: "telemetry-events",
|
|
1882
|
+
event: "runner.telemetry_events.nonfatal",
|
|
1883
|
+
message,
|
|
1884
|
+
context: { source: "sync-runner" },
|
|
1885
|
+
}),
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
catch (err) {
|
|
1890
|
+
reportDiagnostic({
|
|
1891
|
+
component: "qmd-reindex",
|
|
1892
|
+
event: "runner.qmd_reindex.failed",
|
|
1893
|
+
message: "post-sync qmd reindex threw",
|
|
1894
|
+
err,
|
|
1895
|
+
context: { hqRoot: parsed.hqRoot },
|
|
1896
|
+
});
|
|
1897
|
+
// Defensive: reindexAfterSync already swallows internally.
|
|
1898
|
+
void emitCloudTelemetry(client, {
|
|
1899
|
+
eventName: "qmd_reindex_run",
|
|
1900
|
+
source: "sync-runner",
|
|
1901
|
+
sessionId: telemetrySessionId,
|
|
1902
|
+
properties: {
|
|
1903
|
+
result: "threw",
|
|
1904
|
+
changedCount: qmdChangedPaths.length,
|
|
1905
|
+
},
|
|
1906
|
+
}, {
|
|
1907
|
+
claims,
|
|
1908
|
+
log: (message) => reportDiagnostic({
|
|
1909
|
+
component: "telemetry-events",
|
|
1910
|
+
event: "runner.telemetry_events.nonfatal",
|
|
1911
|
+
message,
|
|
1912
|
+
context: { source: "sync-runner" },
|
|
1913
|
+
}),
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1768
1917
|
}
|
|
1769
1918
|
// Post-sync conflict-ledger prune — also a best-effort tail step (runs AFTER
|
|
1770
1919
|
// `all-complete`, never touches the exit code). The producers only append a
|
|
@@ -1989,6 +2138,16 @@ export function loopPassDeps(opts) {
|
|
|
1989
2138
|
...(opts.onJournalsReady ? { onJournalsReady: opts.onJournalsReady } : {}),
|
|
1990
2139
|
...(opts.onAppliedUpserts ? { onAppliedUpserts: opts.onAppliedUpserts } : {}),
|
|
1991
2140
|
...(opts.onAppliedDeletes ? { onAppliedDeletes: opts.onAppliedDeletes } : {}),
|
|
2141
|
+
...(opts.onCatchupSkip ? { onCatchupSkip: opts.onCatchupSkip } : {}),
|
|
2142
|
+
...(opts.onCatchupGrantCompanies
|
|
2143
|
+
? { onCatchupGrantCompanies: opts.onCatchupGrantCompanies }
|
|
2144
|
+
: {}),
|
|
2145
|
+
...(opts.onCatchupGrantVersion
|
|
2146
|
+
? { onCatchupGrantVersion: opts.onCatchupGrantVersion }
|
|
2147
|
+
: {}),
|
|
2148
|
+
...(opts.onCatchupOntologyPush
|
|
2149
|
+
? { onCatchupOntologyPush: opts.onCatchupOntologyPush }
|
|
2150
|
+
: {}),
|
|
1992
2151
|
...(opts.watch ? { recordPassSummary: true } : {}),
|
|
1993
2152
|
onPassResult: opts.onPassResult,
|
|
1994
2153
|
};
|
|
@@ -2004,25 +2163,109 @@ function defaultParentExists(pid) {
|
|
|
2004
2163
|
}
|
|
2005
2164
|
export async function runRunnerWithLoop(argv, deps = {}) {
|
|
2006
2165
|
const parsed = parseArgs(argv);
|
|
2166
|
+
const watcherCatchupSkipStore = deps.watcherCatchupSkipStore ?? ("error" in parsed
|
|
2167
|
+
? undefined
|
|
2168
|
+
: new WatcherCatchupSkipStore(watcherCatchupSkipStorePath(getStateDir(), parsed.hqRoot)));
|
|
2169
|
+
const skipStoreHqRoot = "error" in parsed ? undefined : parsed.hqRoot;
|
|
2170
|
+
let lastSkipStorePruneAt = 0;
|
|
2171
|
+
let skipStorePruneWarningReported = false;
|
|
2007
2172
|
const initialParentPid = process.ppid;
|
|
2008
2173
|
const parentPid = "error" in parsed ? initialParentPid : (parsed.watchParentPid ?? initialParentPid);
|
|
2009
2174
|
const runPassWithOperationLockAlreadyHeld = async (passArgv, fanoutHeartbeatIntervalMs, checkpoint, onJournalsReady, onAppliedUpserts, transferPriority = "chore", watch = false, qmdSignal, qmdHeartbeat, qmdHeartbeatIntervalMs, onAppliedDeletes) => {
|
|
2010
2175
|
let result;
|
|
2011
|
-
const
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2176
|
+
const passStartedAt = Date.now();
|
|
2177
|
+
const pendingCatchupSkips = [];
|
|
2178
|
+
let exitCode = 1;
|
|
2179
|
+
try {
|
|
2180
|
+
exitCode = await runRunner(passArgv, loopPassDeps({
|
|
2181
|
+
watch,
|
|
2182
|
+
transferPriority,
|
|
2183
|
+
qmdSignal,
|
|
2184
|
+
qmdHeartbeat,
|
|
2185
|
+
qmdHeartbeatIntervalMs,
|
|
2186
|
+
fanoutHeartbeatIntervalMs,
|
|
2187
|
+
checkpoint,
|
|
2188
|
+
onJournalsReady,
|
|
2189
|
+
onAppliedUpserts,
|
|
2190
|
+
onAppliedDeletes,
|
|
2191
|
+
...(watcherCatchupSkipStore
|
|
2192
|
+
? { onCatchupSkip: (record) => pendingCatchupSkips.push(record) }
|
|
2193
|
+
: {}),
|
|
2194
|
+
...(watcherCatchupSkipStore
|
|
2195
|
+
? {
|
|
2196
|
+
onCatchupGrantCompanies: (companies) => watcherCatchupSkipStore.setCurrentGrantCompanies(companies),
|
|
2197
|
+
}
|
|
2198
|
+
: {}),
|
|
2199
|
+
...(watcherCatchupSkipStore
|
|
2200
|
+
? {
|
|
2201
|
+
onCatchupOntologyPush: (company, enabled) => watcherCatchupSkipStore.setCurrentOntologyPush(company, enabled),
|
|
2202
|
+
}
|
|
2203
|
+
: {}),
|
|
2204
|
+
...(watcherCatchupSkipStore
|
|
2205
|
+
? {
|
|
2206
|
+
onCatchupGrantVersion: (company, grantVersion) => watcherCatchupSkipStore.setCurrentGrantVersion(company, grantVersion),
|
|
2207
|
+
}
|
|
2208
|
+
: {}),
|
|
2209
|
+
onPassResult: (passResult) => {
|
|
2210
|
+
result = passResult;
|
|
2211
|
+
},
|
|
2212
|
+
}));
|
|
2213
|
+
if (exitCode === 0) {
|
|
2214
|
+
watcherCatchupSkipStore?.clearResolvedGrantCompanies();
|
|
2215
|
+
const companyIndex = passArgv.indexOf("--company");
|
|
2216
|
+
const company = companyIndex >= 0 ? passArgv[companyIndex + 1]?.trim() : undefined;
|
|
2217
|
+
if (company)
|
|
2218
|
+
watcherCatchupSkipStore?.clearUnresolvedCompany(company);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
finally {
|
|
2222
|
+
if (watcherCatchupSkipStore &&
|
|
2223
|
+
skipStoreHqRoot &&
|
|
2224
|
+
Date.now() - lastSkipStorePruneAt >= 6 * 60 * 60 * 1_000) {
|
|
2225
|
+
watcherCatchupSkipStore.pruneMissing((record) => {
|
|
2226
|
+
const relativePath = record.company === "personal"
|
|
2227
|
+
? record.path
|
|
2228
|
+
: ["companies", record.company, record.path].join("/");
|
|
2229
|
+
try {
|
|
2230
|
+
fs.lstatSync(localPathForVaultKey(skipStoreHqRoot, relativePath));
|
|
2231
|
+
return true;
|
|
2232
|
+
}
|
|
2233
|
+
catch (error) {
|
|
2234
|
+
const code = error.code;
|
|
2235
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
2236
|
+
return false;
|
|
2237
|
+
if (!skipStorePruneWarningReported) {
|
|
2238
|
+
skipStorePruneWarningReported = true;
|
|
2239
|
+
process.stderr.write(`hq-sync-runner: catch-up skip pruning could not inspect a local path; retaining record: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2240
|
+
}
|
|
2241
|
+
return true;
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
lastSkipStorePruneAt = Date.now();
|
|
2245
|
+
}
|
|
2246
|
+
if (watcherCatchupSkipStore) {
|
|
2247
|
+
const recordedAt = Date.now();
|
|
2248
|
+
const unique = new Map();
|
|
2249
|
+
for (const record of pendingCatchupSkips) {
|
|
2250
|
+
unique.set(`${record.company}\u0000${record.path}`, record);
|
|
2251
|
+
}
|
|
2252
|
+
watcherCatchupSkipStore.commit([...unique.values()].map((record) => ({ ...record, recordedAt })));
|
|
2253
|
+
const counts = {
|
|
2254
|
+
"archived-session-log": 0,
|
|
2255
|
+
"access-denied": 0,
|
|
2256
|
+
"cap-evicted": watcherCatchupSkipStore.takeCapEvictedCount(),
|
|
2257
|
+
};
|
|
2258
|
+
for (const record of unique.values())
|
|
2259
|
+
counts[record.reason] += 1;
|
|
2260
|
+
process.stderr.write(`${JSON.stringify({
|
|
2261
|
+
type: "watcher-push-skip-summary",
|
|
2262
|
+
diagnostic: true,
|
|
2263
|
+
elapsedMs: Date.now() - passStartedAt,
|
|
2264
|
+
recordedPaths: unique.size,
|
|
2265
|
+
countsByReason: counts,
|
|
2266
|
+
})}\n`);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2026
2269
|
return { exitCode, ...(result ? { result } : {}) };
|
|
2027
2270
|
};
|
|
2028
2271
|
const runtime = {
|
|
@@ -2051,6 +2294,15 @@ export async function runRunnerWithLoop(argv, deps = {}) {
|
|
|
2051
2294
|
const ownerLease = ownerResult.lease;
|
|
2052
2295
|
const restoreOperationLockSignalReraise = suppressOperationLockSignalReraise();
|
|
2053
2296
|
const protocolOutput = deps.stdout ?? process.stdout;
|
|
2297
|
+
let latestSyncWorkState = {
|
|
2298
|
+
passActive: false,
|
|
2299
|
+
queuedWatcherPushes: 0,
|
|
2300
|
+
inFlightWatcherPushes: 0,
|
|
2301
|
+
pendingReceivers: 0,
|
|
2302
|
+
};
|
|
2303
|
+
const writeSyncWorkState = () => {
|
|
2304
|
+
protocolOutput.write(`${JSON.stringify({ type: "sync-work-state", ...latestSyncWorkState })}\n`);
|
|
2305
|
+
};
|
|
2054
2306
|
let protocolStatus = { state: "starting" };
|
|
2055
2307
|
const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
|
|
2056
2308
|
eventLoopDelay.enable();
|
|
@@ -2110,6 +2362,7 @@ export async function runRunnerWithLoop(argv, deps = {}) {
|
|
|
2110
2362
|
else {
|
|
2111
2363
|
protocolOutput.write(line);
|
|
2112
2364
|
}
|
|
2365
|
+
writeSyncWorkState();
|
|
2113
2366
|
if (delay.maxMs > 2_000 && !stallReported) {
|
|
2114
2367
|
stallReported = true;
|
|
2115
2368
|
(deps.stderr ?? process.stderr).write(`${JSON.stringify({
|
|
@@ -2262,7 +2515,9 @@ export async function runRunnerWithLoop(argv, deps = {}) {
|
|
|
2262
2515
|
while (!stopped) {
|
|
2263
2516
|
const code = await runWatchLoop(argv, parsed, {
|
|
2264
2517
|
...deps,
|
|
2518
|
+
...(watcherCatchupSkipStore ? { watcherCatchupSkipStore } : {}),
|
|
2265
2519
|
reportProtocolStatus,
|
|
2520
|
+
onSyncWorkStateSnapshot: (snapshot) => { latestSyncWorkState = snapshot; },
|
|
2266
2521
|
onShutdownSignal: (handler) => {
|
|
2267
2522
|
innerShutdown = handler;
|
|
2268
2523
|
if (stopped)
|