@indigoai-us/hq-cloud 6.18.52 → 6.18.55-alpha.0
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 +348 -94
- 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/share.d.ts +27 -0
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +119 -22
- 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 +74 -14
- 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-snapshot-v4.d.ts.map +1 -1
- package/dist/journal-snapshot-v4.js +44 -38
- package/dist/journal-snapshot-v4.js.map +1 -1
- package/dist/journal.d.ts +15 -2
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +221 -16
- 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/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/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 +1 -1
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,7 +105,8 @@ 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";
|
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -1286,16 +1351,17 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1286
1351
|
for (const target of plan) {
|
|
1287
1352
|
const journalSlug = target.journalSlug ?? target.slug;
|
|
1288
1353
|
if (!journalTargets.has(journalSlug))
|
|
1289
|
-
journalTargets.set(journalSlug, target.slug);
|
|
1354
|
+
journalTargets.set(journalSlug, { company: target.slug, companyUid: target.uid });
|
|
1290
1355
|
}
|
|
1291
1356
|
// The one-time personal-vault seed below reads the legacy `personal` shard
|
|
1292
1357
|
// even when no real company with that slug appears in the fanout plan. Give
|
|
1293
1358
|
// that source the same bounded recovery before migration can open it. A
|
|
1294
1359
|
// missing shard is a metadata-only no-op.
|
|
1295
1360
|
if (!journalTargets.has("personal"))
|
|
1296
|
-
journalTargets.set("personal", "personal");
|
|
1361
|
+
journalTargets.set("personal", { company: "personal", companyUid: "" });
|
|
1297
1362
|
const skippedJournalShards = new Set();
|
|
1298
|
-
for (const [journalSlug,
|
|
1363
|
+
for (const [journalSlug, targetIdentity] of journalTargets) {
|
|
1364
|
+
const { company, companyUid } = targetIdentity;
|
|
1299
1365
|
// Area-authoritative layouts rotate and repair through the watch loop's
|
|
1300
1366
|
// bounded chore cursor. Do not put every area behind this pass's startup
|
|
1301
1367
|
// preflight: a realtime scoped pass must be able to start before a
|
|
@@ -1306,6 +1372,7 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1306
1372
|
}
|
|
1307
1373
|
try {
|
|
1308
1374
|
repairJournalStateIfNeeded(journalSlug, {
|
|
1375
|
+
...(companyUid ? { companyUid } : {}),
|
|
1309
1376
|
onProgress: ({ bytesRead, totalBytes }) => {
|
|
1310
1377
|
emit({
|
|
1311
1378
|
type: "maintenance-progress",
|
|
@@ -1333,8 +1400,12 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1333
1400
|
}
|
|
1334
1401
|
emit({
|
|
1335
1402
|
type: "error",
|
|
1336
|
-
|
|
1337
|
-
|
|
1403
|
+
...(companyUid ? { companyUid } : {}),
|
|
1404
|
+
errorClass: err instanceof Error && err.message.includes("HQSNAP4")
|
|
1405
|
+
? "HQSNAP4EncodeError"
|
|
1406
|
+
: "JournalCompactionError",
|
|
1407
|
+
message: "journal recovery failed",
|
|
1408
|
+
path: "(journal)",
|
|
1338
1409
|
});
|
|
1339
1410
|
emitRunnerTerminalTelemetry({ status: "failed" });
|
|
1340
1411
|
return 1;
|
|
@@ -1345,7 +1416,9 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1345
1416
|
// does NOT trigger a mass re-upload of the HQ overlay on first run. No-op
|
|
1346
1417
|
// when the new journal already exists or the legacy one is absent, and
|
|
1347
1418
|
// harmless even when the personal-vault slot is not in the plan.
|
|
1348
|
-
if (!
|
|
1419
|
+
if (!parsed.preflightLimits &&
|
|
1420
|
+
!skippedJournalShards.has("personal") &&
|
|
1421
|
+
!skippedJournalShards.has(PERSONAL_VAULT_JOURNAL_SLUG)) {
|
|
1349
1422
|
(deps.migratePersonalVaultJournal ?? migratePersonalVaultJournal)();
|
|
1350
1423
|
}
|
|
1351
1424
|
// The journal preflight and personal-vault seed are the point at which an
|
|
@@ -1360,6 +1433,16 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1360
1433
|
// ---- fanout -----------------------------------------------------------
|
|
1361
1434
|
const syncFn = deps.sync ?? defaultSync;
|
|
1362
1435
|
const shareFn = deps.share ?? defaultShare;
|
|
1436
|
+
let audit;
|
|
1437
|
+
if (parsed.auditHashOut) {
|
|
1438
|
+
try {
|
|
1439
|
+
audit = new SyncRunnerHashAudit(parsed.auditHashOut);
|
|
1440
|
+
}
|
|
1441
|
+
catch {
|
|
1442
|
+
emit({ type: "error", path: "(audit)", message: "could not create hash audit output" });
|
|
1443
|
+
return 1;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1363
1446
|
let fanout;
|
|
1364
1447
|
try {
|
|
1365
1448
|
const heartbeatCompany = plan[0]?.slug ?? "(fanout)";
|
|
@@ -1403,6 +1486,9 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1403
1486
|
fanout = await executeCompanyFanout({
|
|
1404
1487
|
plan,
|
|
1405
1488
|
direction: parsed.direction,
|
|
1489
|
+
...(parsed.preflightLimits ? { preflightLimits: parsed.preflightLimits } : {}),
|
|
1490
|
+
grantInventoryComplete: parsed.companies,
|
|
1491
|
+
...(audit ? { audit } : {}),
|
|
1406
1492
|
hqRoot: parsed.hqRoot,
|
|
1407
1493
|
onConflict: parsed.onConflict,
|
|
1408
1494
|
client,
|
|
@@ -1416,6 +1502,16 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1416
1502
|
: {}),
|
|
1417
1503
|
syncFn,
|
|
1418
1504
|
shareFn,
|
|
1505
|
+
...(deps.onCatchupSkip ? { onCatchupSkip: deps.onCatchupSkip } : {}),
|
|
1506
|
+
...(deps.onCatchupGrantCompanies
|
|
1507
|
+
? { onCatchupGrantCompanies: deps.onCatchupGrantCompanies }
|
|
1508
|
+
: {}),
|
|
1509
|
+
...(deps.onCatchupGrantVersion
|
|
1510
|
+
? { onCatchupGrantVersion: deps.onCatchupGrantVersion }
|
|
1511
|
+
: {}),
|
|
1512
|
+
...(deps.onCatchupOntologyPush
|
|
1513
|
+
? { onCatchupOntologyPush: deps.onCatchupOntologyPush }
|
|
1514
|
+
: {}),
|
|
1419
1515
|
resolveDeletePolicy,
|
|
1420
1516
|
emit,
|
|
1421
1517
|
...(emitFanoutHeartbeat ? { onHeartbeat: emitFanoutHeartbeat } : {}),
|
|
@@ -1443,6 +1539,7 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1443
1539
|
finally {
|
|
1444
1540
|
if (heartbeatTimer)
|
|
1445
1541
|
clearInterval(heartbeatTimer);
|
|
1542
|
+
audit?.close();
|
|
1446
1543
|
}
|
|
1447
1544
|
}
|
|
1448
1545
|
catch (err) {
|
|
@@ -1468,6 +1565,8 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1468
1565
|
}
|
|
1469
1566
|
throw err;
|
|
1470
1567
|
}
|
|
1568
|
+
if (fanout.preflightHalted)
|
|
1569
|
+
return PREFLIGHT_HALT_EXIT;
|
|
1471
1570
|
const { errors, transientCompanies, allConflicts, meaningfulOutcomeCount } = fanout;
|
|
1472
1571
|
const rollup = rollupAllComplete(plan, fanout.stateByCompany);
|
|
1473
1572
|
deps.onPassResult?.({
|
|
@@ -1689,82 +1788,131 @@ export async function runRunner(argv, deps = {}) {
|
|
|
1689
1788
|
const qmdChangedPaths = rollup.changedPaths.filter(isQmdIndexableContentPath);
|
|
1690
1789
|
if (qmdChangedPaths.length > 0 &&
|
|
1691
1790
|
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) {
|
|
1791
|
+
const qmdAdmission = deps.qmdAdmission;
|
|
1792
|
+
const qmdNow = qmdAdmission?.now ?? Date.now;
|
|
1793
|
+
const qmdPlatform = qmdAdmission?.platform ?? process.platform;
|
|
1794
|
+
const qmdEnv = qmdAdmission?.env ?? process.env;
|
|
1795
|
+
const qmdHome = qmdAdmission?.home ?? qmdEnv.HOME ?? os.homedir();
|
|
1796
|
+
const qmdRunnerFinishedByRoot = deps.qmdRunnerFinishedByRoot ?? (deps.qmdRunnerFinishedByRoot = new Map());
|
|
1797
|
+
const qmdLockHeld = isQmdBackgroundLockHeld(qmdHome, { nowMs: qmdNow() });
|
|
1798
|
+
if (qmdRunnerShouldSkip({
|
|
1799
|
+
platform: qmdPlatform,
|
|
1800
|
+
env: qmdEnv,
|
|
1801
|
+
nowMs: qmdNow(),
|
|
1802
|
+
lastFinishedAt: qmdRunnerLastFinishedAt(parsed.hqRoot, qmdRunnerFinishedByRoot),
|
|
1803
|
+
lockHeld: qmdLockHeld,
|
|
1804
|
+
fullRefresh: parsed.qmdFullRefresh,
|
|
1805
|
+
})) {
|
|
1742
1806
|
reportDiagnostic({
|
|
1743
1807
|
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
|
-
}),
|
|
1808
|
+
event: "runner.qmd_reindex.skipped_admission",
|
|
1809
|
+
message: qmdLockHeld
|
|
1810
|
+
? "post-sync qmd reindex skipped because the hq-cli background lock is held"
|
|
1811
|
+
: "post-sync qmd reindex skipped because the minimum interval has not elapsed",
|
|
1812
|
+
context: { hqRoot: parsed.hqRoot, lockBusy: qmdLockHeld },
|
|
1766
1813
|
});
|
|
1767
1814
|
}
|
|
1815
|
+
else {
|
|
1816
|
+
try {
|
|
1817
|
+
const releaseSharedLock = acquireQmdBackgroundLock(qmdHome, qmdNow());
|
|
1818
|
+
if (!releaseSharedLock) {
|
|
1819
|
+
reportDiagnostic({
|
|
1820
|
+
component: "qmd-reindex",
|
|
1821
|
+
event: "runner.qmd_reindex.skipped_admission",
|
|
1822
|
+
message: "post-sync qmd reindex skipped because the hq-cli background lock could not be acquired",
|
|
1823
|
+
context: { hqRoot: parsed.hqRoot, lockBusy: true },
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
else {
|
|
1827
|
+
let completedQmdResult;
|
|
1828
|
+
try {
|
|
1829
|
+
const qmdResult = (deps.qmdReindex ?? reindexAfterSync)(parsed.hqRoot, {
|
|
1830
|
+
changedPaths: qmdChangedPaths,
|
|
1831
|
+
forceCollectionRefresh: true,
|
|
1832
|
+
signal: deps.qmdSignal,
|
|
1833
|
+
...(deps.qmdHeartbeat
|
|
1834
|
+
? {
|
|
1835
|
+
onHeartbeat: deps.qmdHeartbeat,
|
|
1836
|
+
heartbeatIntervalMs: deps.qmdHeartbeatIntervalMs,
|
|
1837
|
+
}
|
|
1838
|
+
: {}),
|
|
1839
|
+
log: ({ event, message, err, context }) => reportDiagnostic({
|
|
1840
|
+
component: "qmd-reindex",
|
|
1841
|
+
event,
|
|
1842
|
+
message,
|
|
1843
|
+
err,
|
|
1844
|
+
context: { hqRoot: parsed.hqRoot, ...(context ?? {}) },
|
|
1845
|
+
}),
|
|
1846
|
+
});
|
|
1847
|
+
completedQmdResult = await qmdResult;
|
|
1848
|
+
}
|
|
1849
|
+
finally {
|
|
1850
|
+
releaseSharedLock();
|
|
1851
|
+
}
|
|
1852
|
+
if (completedQmdResult.qmdAvailable &&
|
|
1853
|
+
completedQmdResult.updated &&
|
|
1854
|
+
completedQmdResult.lockBusy !== true &&
|
|
1855
|
+
completedQmdResult.timedOut !== true) {
|
|
1856
|
+
recordQmdRunnerFinished(parsed.hqRoot, qmdNow(), qmdRunnerFinishedByRoot);
|
|
1857
|
+
}
|
|
1858
|
+
void emitCloudTelemetry(client, {
|
|
1859
|
+
eventName: "qmd_reindex_run",
|
|
1860
|
+
source: "sync-runner",
|
|
1861
|
+
sessionId: telemetrySessionId,
|
|
1862
|
+
properties: {
|
|
1863
|
+
result: "completed",
|
|
1864
|
+
changedCount: qmdChangedPaths.length,
|
|
1865
|
+
qmdState: completedQmdResult.qmdAvailable ? "available" : "unavailable",
|
|
1866
|
+
collectionsAddedCount: completedQmdResult.collectionsAdded.length,
|
|
1867
|
+
pathDriftDetectedCount: completedQmdResult.pathDriftDetected?.length ?? 0,
|
|
1868
|
+
collectionsRepairedCount: completedQmdResult.collectionsRepaired?.length ?? 0,
|
|
1869
|
+
updateState: completedQmdResult.updated ? "updated" : "unchanged",
|
|
1870
|
+
embedState: completedQmdResult.embedded ? "embedded" : "not_embedded",
|
|
1871
|
+
dirtyState: completedQmdResult.pendingDirty ? "pending_dirty" : "clean",
|
|
1872
|
+
lockBusy: completedQmdResult.lockBusy === true ? 1 : 0,
|
|
1873
|
+
timedOut: completedQmdResult.timedOut === true ? 1 : 0,
|
|
1874
|
+
corruptionQuarantined: completedQmdResult.corruptionQuarantined === true ? 1 : 0,
|
|
1875
|
+
corruptionQuarantineFailed: completedQmdResult.corruptionQuarantineFailed === true ? 1 : 0,
|
|
1876
|
+
},
|
|
1877
|
+
}, {
|
|
1878
|
+
claims,
|
|
1879
|
+
log: (message) => reportDiagnostic({
|
|
1880
|
+
component: "telemetry-events",
|
|
1881
|
+
event: "runner.telemetry_events.nonfatal",
|
|
1882
|
+
message,
|
|
1883
|
+
context: { source: "sync-runner" },
|
|
1884
|
+
}),
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
catch (err) {
|
|
1889
|
+
reportDiagnostic({
|
|
1890
|
+
component: "qmd-reindex",
|
|
1891
|
+
event: "runner.qmd_reindex.failed",
|
|
1892
|
+
message: "post-sync qmd reindex threw",
|
|
1893
|
+
err,
|
|
1894
|
+
context: { hqRoot: parsed.hqRoot },
|
|
1895
|
+
});
|
|
1896
|
+
// Defensive: reindexAfterSync already swallows internally.
|
|
1897
|
+
void emitCloudTelemetry(client, {
|
|
1898
|
+
eventName: "qmd_reindex_run",
|
|
1899
|
+
source: "sync-runner",
|
|
1900
|
+
sessionId: telemetrySessionId,
|
|
1901
|
+
properties: {
|
|
1902
|
+
result: "threw",
|
|
1903
|
+
changedCount: qmdChangedPaths.length,
|
|
1904
|
+
},
|
|
1905
|
+
}, {
|
|
1906
|
+
claims,
|
|
1907
|
+
log: (message) => reportDiagnostic({
|
|
1908
|
+
component: "telemetry-events",
|
|
1909
|
+
event: "runner.telemetry_events.nonfatal",
|
|
1910
|
+
message,
|
|
1911
|
+
context: { source: "sync-runner" },
|
|
1912
|
+
}),
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1768
1916
|
}
|
|
1769
1917
|
// Post-sync conflict-ledger prune — also a best-effort tail step (runs AFTER
|
|
1770
1918
|
// `all-complete`, never touches the exit code). The producers only append a
|
|
@@ -1989,6 +2137,16 @@ export function loopPassDeps(opts) {
|
|
|
1989
2137
|
...(opts.onJournalsReady ? { onJournalsReady: opts.onJournalsReady } : {}),
|
|
1990
2138
|
...(opts.onAppliedUpserts ? { onAppliedUpserts: opts.onAppliedUpserts } : {}),
|
|
1991
2139
|
...(opts.onAppliedDeletes ? { onAppliedDeletes: opts.onAppliedDeletes } : {}),
|
|
2140
|
+
...(opts.onCatchupSkip ? { onCatchupSkip: opts.onCatchupSkip } : {}),
|
|
2141
|
+
...(opts.onCatchupGrantCompanies
|
|
2142
|
+
? { onCatchupGrantCompanies: opts.onCatchupGrantCompanies }
|
|
2143
|
+
: {}),
|
|
2144
|
+
...(opts.onCatchupGrantVersion
|
|
2145
|
+
? { onCatchupGrantVersion: opts.onCatchupGrantVersion }
|
|
2146
|
+
: {}),
|
|
2147
|
+
...(opts.onCatchupOntologyPush
|
|
2148
|
+
? { onCatchupOntologyPush: opts.onCatchupOntologyPush }
|
|
2149
|
+
: {}),
|
|
1992
2150
|
...(opts.watch ? { recordPassSummary: true } : {}),
|
|
1993
2151
|
onPassResult: opts.onPassResult,
|
|
1994
2152
|
};
|
|
@@ -2004,25 +2162,109 @@ function defaultParentExists(pid) {
|
|
|
2004
2162
|
}
|
|
2005
2163
|
export async function runRunnerWithLoop(argv, deps = {}) {
|
|
2006
2164
|
const parsed = parseArgs(argv);
|
|
2165
|
+
const watcherCatchupSkipStore = deps.watcherCatchupSkipStore ?? ("error" in parsed
|
|
2166
|
+
? undefined
|
|
2167
|
+
: new WatcherCatchupSkipStore(watcherCatchupSkipStorePath(getStateDir(), parsed.hqRoot)));
|
|
2168
|
+
const skipStoreHqRoot = "error" in parsed ? undefined : parsed.hqRoot;
|
|
2169
|
+
let lastSkipStorePruneAt = 0;
|
|
2170
|
+
let skipStorePruneWarningReported = false;
|
|
2007
2171
|
const initialParentPid = process.ppid;
|
|
2008
2172
|
const parentPid = "error" in parsed ? initialParentPid : (parsed.watchParentPid ?? initialParentPid);
|
|
2009
2173
|
const runPassWithOperationLockAlreadyHeld = async (passArgv, fanoutHeartbeatIntervalMs, checkpoint, onJournalsReady, onAppliedUpserts, transferPriority = "chore", watch = false, qmdSignal, qmdHeartbeat, qmdHeartbeatIntervalMs, onAppliedDeletes) => {
|
|
2010
2174
|
let result;
|
|
2011
|
-
const
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2175
|
+
const passStartedAt = Date.now();
|
|
2176
|
+
const pendingCatchupSkips = [];
|
|
2177
|
+
let exitCode = 1;
|
|
2178
|
+
try {
|
|
2179
|
+
exitCode = await runRunner(passArgv, loopPassDeps({
|
|
2180
|
+
watch,
|
|
2181
|
+
transferPriority,
|
|
2182
|
+
qmdSignal,
|
|
2183
|
+
qmdHeartbeat,
|
|
2184
|
+
qmdHeartbeatIntervalMs,
|
|
2185
|
+
fanoutHeartbeatIntervalMs,
|
|
2186
|
+
checkpoint,
|
|
2187
|
+
onJournalsReady,
|
|
2188
|
+
onAppliedUpserts,
|
|
2189
|
+
onAppliedDeletes,
|
|
2190
|
+
...(watcherCatchupSkipStore
|
|
2191
|
+
? { onCatchupSkip: (record) => pendingCatchupSkips.push(record) }
|
|
2192
|
+
: {}),
|
|
2193
|
+
...(watcherCatchupSkipStore
|
|
2194
|
+
? {
|
|
2195
|
+
onCatchupGrantCompanies: (companies) => watcherCatchupSkipStore.setCurrentGrantCompanies(companies),
|
|
2196
|
+
}
|
|
2197
|
+
: {}),
|
|
2198
|
+
...(watcherCatchupSkipStore
|
|
2199
|
+
? {
|
|
2200
|
+
onCatchupOntologyPush: (company, enabled) => watcherCatchupSkipStore.setCurrentOntologyPush(company, enabled),
|
|
2201
|
+
}
|
|
2202
|
+
: {}),
|
|
2203
|
+
...(watcherCatchupSkipStore
|
|
2204
|
+
? {
|
|
2205
|
+
onCatchupGrantVersion: (company, grantVersion) => watcherCatchupSkipStore.setCurrentGrantVersion(company, grantVersion),
|
|
2206
|
+
}
|
|
2207
|
+
: {}),
|
|
2208
|
+
onPassResult: (passResult) => {
|
|
2209
|
+
result = passResult;
|
|
2210
|
+
},
|
|
2211
|
+
}));
|
|
2212
|
+
if (exitCode === 0) {
|
|
2213
|
+
watcherCatchupSkipStore?.clearResolvedGrantCompanies();
|
|
2214
|
+
const companyIndex = passArgv.indexOf("--company");
|
|
2215
|
+
const company = companyIndex >= 0 ? passArgv[companyIndex + 1]?.trim() : undefined;
|
|
2216
|
+
if (company)
|
|
2217
|
+
watcherCatchupSkipStore?.clearUnresolvedCompany(company);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
finally {
|
|
2221
|
+
if (watcherCatchupSkipStore &&
|
|
2222
|
+
skipStoreHqRoot &&
|
|
2223
|
+
Date.now() - lastSkipStorePruneAt >= 6 * 60 * 60 * 1_000) {
|
|
2224
|
+
watcherCatchupSkipStore.pruneMissing((record) => {
|
|
2225
|
+
const relativePath = record.company === "personal"
|
|
2226
|
+
? record.path
|
|
2227
|
+
: ["companies", record.company, record.path].join("/");
|
|
2228
|
+
try {
|
|
2229
|
+
fs.lstatSync(localPathForVaultKey(skipStoreHqRoot, relativePath));
|
|
2230
|
+
return true;
|
|
2231
|
+
}
|
|
2232
|
+
catch (error) {
|
|
2233
|
+
const code = error.code;
|
|
2234
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
2235
|
+
return false;
|
|
2236
|
+
if (!skipStorePruneWarningReported) {
|
|
2237
|
+
skipStorePruneWarningReported = true;
|
|
2238
|
+
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`);
|
|
2239
|
+
}
|
|
2240
|
+
return true;
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2243
|
+
lastSkipStorePruneAt = Date.now();
|
|
2244
|
+
}
|
|
2245
|
+
if (watcherCatchupSkipStore) {
|
|
2246
|
+
const recordedAt = Date.now();
|
|
2247
|
+
const unique = new Map();
|
|
2248
|
+
for (const record of pendingCatchupSkips) {
|
|
2249
|
+
unique.set(`${record.company}\u0000${record.path}`, record);
|
|
2250
|
+
}
|
|
2251
|
+
watcherCatchupSkipStore.commit([...unique.values()].map((record) => ({ ...record, recordedAt })));
|
|
2252
|
+
const counts = {
|
|
2253
|
+
"archived-session-log": 0,
|
|
2254
|
+
"access-denied": 0,
|
|
2255
|
+
"cap-evicted": watcherCatchupSkipStore.takeCapEvictedCount(),
|
|
2256
|
+
};
|
|
2257
|
+
for (const record of unique.values())
|
|
2258
|
+
counts[record.reason] += 1;
|
|
2259
|
+
process.stderr.write(`${JSON.stringify({
|
|
2260
|
+
type: "watcher-push-skip-summary",
|
|
2261
|
+
diagnostic: true,
|
|
2262
|
+
elapsedMs: Date.now() - passStartedAt,
|
|
2263
|
+
recordedPaths: unique.size,
|
|
2264
|
+
countsByReason: counts,
|
|
2265
|
+
})}\n`);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2026
2268
|
return { exitCode, ...(result ? { result } : {}) };
|
|
2027
2269
|
};
|
|
2028
2270
|
const runtime = {
|
|
@@ -2051,6 +2293,15 @@ export async function runRunnerWithLoop(argv, deps = {}) {
|
|
|
2051
2293
|
const ownerLease = ownerResult.lease;
|
|
2052
2294
|
const restoreOperationLockSignalReraise = suppressOperationLockSignalReraise();
|
|
2053
2295
|
const protocolOutput = deps.stdout ?? process.stdout;
|
|
2296
|
+
let latestSyncWorkState = {
|
|
2297
|
+
passActive: false,
|
|
2298
|
+
queuedWatcherPushes: 0,
|
|
2299
|
+
inFlightWatcherPushes: 0,
|
|
2300
|
+
pendingReceivers: 0,
|
|
2301
|
+
};
|
|
2302
|
+
const writeSyncWorkState = () => {
|
|
2303
|
+
protocolOutput.write(`${JSON.stringify({ type: "sync-work-state", ...latestSyncWorkState })}\n`);
|
|
2304
|
+
};
|
|
2054
2305
|
let protocolStatus = { state: "starting" };
|
|
2055
2306
|
const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
|
|
2056
2307
|
eventLoopDelay.enable();
|
|
@@ -2110,6 +2361,7 @@ export async function runRunnerWithLoop(argv, deps = {}) {
|
|
|
2110
2361
|
else {
|
|
2111
2362
|
protocolOutput.write(line);
|
|
2112
2363
|
}
|
|
2364
|
+
writeSyncWorkState();
|
|
2113
2365
|
if (delay.maxMs > 2_000 && !stallReported) {
|
|
2114
2366
|
stallReported = true;
|
|
2115
2367
|
(deps.stderr ?? process.stderr).write(`${JSON.stringify({
|
|
@@ -2262,7 +2514,9 @@ export async function runRunnerWithLoop(argv, deps = {}) {
|
|
|
2262
2514
|
while (!stopped) {
|
|
2263
2515
|
const code = await runWatchLoop(argv, parsed, {
|
|
2264
2516
|
...deps,
|
|
2517
|
+
...(watcherCatchupSkipStore ? { watcherCatchupSkipStore } : {}),
|
|
2265
2518
|
reportProtocolStatus,
|
|
2519
|
+
onSyncWorkStateSnapshot: (snapshot) => { latestSyncWorkState = snapshot; },
|
|
2266
2520
|
onShutdownSignal: (handler) => {
|
|
2267
2521
|
innerShutdown = handler;
|
|
2268
2522
|
if (stopped)
|