@indigoai-us/hq-cloud 6.18.94 → 6.18.95
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.d.ts +20 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +25 -0
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/cli/reindex.d.ts +24 -0
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +26 -1
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/share.d.ts +28 -5
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +142 -19
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/sync.d.ts +11 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +56 -7
- package/dist/cli/sync.js.map +1 -1
- package/dist/hq-root-recovery.d.ts +37 -6
- package/dist/hq-root-recovery.d.ts.map +1 -1
- package/dist/hq-root-recovery.js +266 -22
- package/dist/hq-root-recovery.js.map +1 -1
- package/dist/journal.d.ts +11 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +23 -2
- package/dist/journal.js.map +1 -1
- package/dist/lib/cloud-authoritative.d.ts +10 -0
- package/dist/lib/cloud-authoritative.d.ts.map +1 -1
- package/dist/lib/cloud-authoritative.js +23 -0
- package/dist/lib/cloud-authoritative.js.map +1 -1
- package/dist/manifest-reconcile.d.ts +14 -0
- package/dist/manifest-reconcile.d.ts.map +1 -1
- package/dist/manifest-reconcile.js +20 -2
- package/dist/manifest-reconcile.js.map +1 -1
- package/dist/outposts/client.d.ts +6 -1
- package/dist/outposts/client.d.ts.map +1 -1
- package/dist/outposts/client.js +13 -1
- package/dist/outposts/client.js.map +1 -1
- package/dist/outposts/command.d.ts +1 -1
- package/dist/outposts/command.d.ts.map +1 -1
- package/dist/outposts/command.js +58 -2
- package/dist/outposts/command.js.map +1 -1
- package/dist/outposts/company.d.ts +15 -1
- package/dist/outposts/company.d.ts.map +1 -1
- package/dist/outposts/company.js +36 -1
- package/dist/outposts/company.js.map +1 -1
- package/dist/s3.d.ts +22 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +44 -0
- package/dist/s3.js.map +1 -1
- package/dist/session-log-capture.d.ts.map +1 -1
- package/dist/session-log-capture.js +5 -2
- package/dist/session-log-capture.js.map +1 -1
- package/dist/sync/area-ledger.d.ts.map +1 -1
- package/dist/sync/area-ledger.js +3 -3
- package/dist/sync/area-ledger.js.map +1 -1
- package/dist/sync/durable-apply.js +2 -2
- package/dist/sync/durable-apply.js.map +1 -1
- package/dist/sync/per-area-maintenance.js +2 -2
- package/dist/sync/per-area-maintenance.js.map +1 -1
- package/dist/sync/reconcile-cursor.js +2 -2
- package/dist/sync/reconcile-cursor.js.map +1 -1
- package/dist/sync/secret-path-policy.d.ts +37 -11
- package/dist/sync/secret-path-policy.d.ts.map +1 -1
- package/dist/sync/secret-path-policy.js +68 -34
- package/dist/sync/secret-path-policy.js.map +1 -1
- package/dist/sync/snapshot-client.js +2 -2
- package/dist/sync/snapshot-client.js.map +1 -1
- package/dist/sync/state-store.d.ts +19 -0
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +184 -4
- package/dist/sync/state-store.js.map +1 -1
- package/dist/sync/uploader-finalization.d.ts.map +1 -1
- package/dist/sync/uploader-finalization.js +2 -2
- package/dist/sync/uploader-finalization.js.map +1 -1
- package/dist/test-fixtures/unknown-journal-generation.d.ts +9 -0
- package/dist/test-fixtures/unknown-journal-generation.d.ts.map +1 -0
- package/dist/test-fixtures/unknown-journal-generation.js +37 -0
- package/dist/test-fixtures/unknown-journal-generation.js.map +1 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +3 -3
- package/dist/watcher.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/share.js
CHANGED
|
@@ -23,10 +23,10 @@ import { uploadFile, uploadSymlink, toPosixKey, headRemoteFile, readVerifiedRemo
|
|
|
23
23
|
import * as crypto from "crypto";
|
|
24
24
|
import { bindObjectIOToContext, PresignObjectIO, RateLimitedError, resolveObjectIO, S3SdkObjectIO, } from "../object-io.js";
|
|
25
25
|
import { primePrivateFolderUpload } from "../private-folder-transport.js";
|
|
26
|
-
import { getJournalPath, isUnderAnyRoot, readJournalForPass, readJournalScoped, readQuarantinedJournalPaths, writeJournal, setJournalSnapshotQuarantineListener, hashFile, hashSymlinkTarget, journalRows, journalEntryMatchesStat, journalFingerprintMatchesStat, dropJournalDecodeWindow, timestampsTooCoarseToTrust, updateEntry, PrematureJournalEntryError, removeEntry, isTombstone, normalizeEtag, PERSONAL_VAULT_JOURNAL_SLUG, migratePersonalVaultJournal, migrateToV2, } from "../journal.js";
|
|
27
|
-
import { createIgnoreExplainer, createIgnoreFilter, hasControlCharacters, isExpectedIgnore, isMachineLocalRuntimePath, isPathExcludedFromSync, isSecretPatternIgnore, isWithinSizeLimit, } from "../ignore.js";
|
|
26
|
+
import { getJournalPath, isUnderAnyRoot, readJournalForPass, readJournalReadOnly, readJournalScoped, readQuarantinedJournalPaths, writeJournal, setJournalSnapshotQuarantineListener, hashFile, hashSymlinkTarget, journalRows, journalEntryMatchesStat, journalFingerprintMatchesStat, dropJournalDecodeWindow, timestampsTooCoarseToTrust, updateEntry, PrematureJournalEntryError, removeEntry, isTombstone, normalizeEtag, PERSONAL_VAULT_JOURNAL_SLUG, migratePersonalVaultJournal, migrateToV2, configureRejectedDeltaRecovery, getJournalRecoveryMarker, } from "../journal.js";
|
|
27
|
+
import { createIgnoreExplainer, createIgnoreFilter, hasControlCharacters, isExpectedIgnore, isHqGeneratedIndexFile, isMachineLocalRuntimePath, isPathExcludedFromSync, isSecretFolderPath, isSecretPatternIgnore, isWithinSizeLimit, } from "../ignore.js";
|
|
28
28
|
import { wrapFilterWithPersonalVaultDefaults, } from "../personal-vault-exclusions.js";
|
|
29
|
-
import {
|
|
29
|
+
import { claimNeverSyncedLocalNotices, formatSecretsNotSyncedLine, isNeverSyncedVaultPath, isSecretBearingVaultPath, isSkipSecretPathsEnabled, isWalRejectedDeltaRecoveryEnabled, } from "../sync/secret-path-policy.js";
|
|
30
30
|
import { getActiveFullPassDecodeWindow, getActiveFullPassUnitSize, } from "../sync/full-pass-caps.js";
|
|
31
31
|
import { isGeneratedCoreMirrorKey, isPersonalVaultDeletionExcluded, isSessionLogDeletionKey, purgeExcludedPersonalVaultJournalEntries, } from "../personal-vault.js";
|
|
32
32
|
import { pickWinner, resolveConflict, usesVersionAwareWinner } from "./conflict.js";
|
|
@@ -885,10 +885,13 @@ export class UnreachablePushPathsError extends Error {
|
|
|
885
885
|
paths;
|
|
886
886
|
/** Per-path reason, keyed by the same original spelling. */
|
|
887
887
|
reasons;
|
|
888
|
-
constructor(unreachable, syncRoot) {
|
|
888
|
+
constructor(unreachable, syncRoot, ignoreRules = new Map()) {
|
|
889
889
|
const paths = [...unreachable.keys()];
|
|
890
890
|
const lines = paths.map((p) => {
|
|
891
891
|
const reason = unreachable.get(p);
|
|
892
|
+
if (reason === "ignored") {
|
|
893
|
+
return ` · ${p} — ${ignoreRules.get(p) ?? "excluded by a sync ignore rule"}; it was not uploaded`;
|
|
894
|
+
}
|
|
892
895
|
if (reason === "outside-company") {
|
|
893
896
|
return ` · ${p} — resolves outside the company folder (${syncRoot})`;
|
|
894
897
|
}
|
|
@@ -1367,6 +1370,10 @@ async function createPushRunContext(options) {
|
|
|
1367
1370
|
? await isSkipSecretPathsEnabled(secretPathFlagConfig, ctx.uid, true)
|
|
1368
1371
|
: await isSkipSecretPathsEnabled(secretPathFlagConfig, ctx.uid)
|
|
1369
1372
|
: preVendedCompanyPush;
|
|
1373
|
+
const walRecoveryEnabled = secretPathFlagConfig
|
|
1374
|
+
? await isWalRejectedDeltaRecoveryEnabled(secretPathFlagConfig, ctx.uid)
|
|
1375
|
+
: true;
|
|
1376
|
+
configureRejectedDeltaRecovery(walRecoveryEnabled);
|
|
1370
1377
|
const ignoreFilter = createIgnoreFilter(hqRoot, {
|
|
1371
1378
|
allowRootMcpConfig: personalConnectorSettingsSync,
|
|
1372
1379
|
});
|
|
@@ -1429,8 +1436,7 @@ async function createPushRunContext(options) {
|
|
|
1429
1436
|
return false;
|
|
1430
1437
|
return baseFilter(filePath, isDir);
|
|
1431
1438
|
};
|
|
1432
|
-
const neverSyncedLocalPath
|
|
1433
|
-
if (neverSyncedLocalPath) {
|
|
1439
|
+
for (const neverSyncedLocalPath of claimNeverSyncedLocalNotices(hqRoot, syncRoot)) {
|
|
1434
1440
|
emit({ type: "secrets-not-synced", path: neverSyncedLocalPath });
|
|
1435
1441
|
}
|
|
1436
1442
|
const shouldSync = options.prefixSet !== undefined
|
|
@@ -1453,6 +1459,7 @@ async function createPushRunContext(options) {
|
|
|
1453
1459
|
// known. Full/cadence callers retain the complete snapshot here.
|
|
1454
1460
|
const scopedJournal = options.scopedJournal === true;
|
|
1455
1461
|
const journal = scopedJournal ? emptyJournalForScopedPass() : readJournalForPass(journalSlug, ctx.uid);
|
|
1462
|
+
const holdRemoteDeletes = getJournalRecoveryMarker(journalSlug) !== undefined;
|
|
1456
1463
|
return {
|
|
1457
1464
|
options,
|
|
1458
1465
|
ontologyEntityPush: false,
|
|
@@ -1481,7 +1488,15 @@ async function createPushRunContext(options) {
|
|
|
1481
1488
|
syncRoot,
|
|
1482
1489
|
shouldSync,
|
|
1483
1490
|
durableShouldSync: baseFilter,
|
|
1491
|
+
ignoreFilter,
|
|
1492
|
+
explainIgnore: (filePath, isDir) => {
|
|
1493
|
+
explainIgnored ??= createIgnoreExplainer(hqRoot, {
|
|
1494
|
+
allowRootMcpConfig: personalConnectorSettingsSync,
|
|
1495
|
+
});
|
|
1496
|
+
return explainIgnored(filePath, isDir);
|
|
1497
|
+
},
|
|
1484
1498
|
skipSecretPaths,
|
|
1499
|
+
holdRemoteDeletes,
|
|
1485
1500
|
journalSlug,
|
|
1486
1501
|
quarantinedPaths,
|
|
1487
1502
|
journal,
|
|
@@ -1489,12 +1504,14 @@ async function createPushRunContext(options) {
|
|
|
1489
1504
|
excludedSet,
|
|
1490
1505
|
excludedById,
|
|
1491
1506
|
scopeExcludedSet,
|
|
1507
|
+
uploadFailedSet: new Set(),
|
|
1492
1508
|
ignoreExcludedSet,
|
|
1493
1509
|
ignoreExcludedTotal,
|
|
1494
1510
|
ignoreExcludedLayers,
|
|
1495
1511
|
ignoreIncludeLines,
|
|
1496
1512
|
secretExcludedSet,
|
|
1497
1513
|
unreachablePaths,
|
|
1514
|
+
ignoredNamedPathRules: new Map(),
|
|
1498
1515
|
linkedSubtreeSet,
|
|
1499
1516
|
skillRegistrationConflicts,
|
|
1500
1517
|
hostLoadAtStart,
|
|
@@ -2184,6 +2201,7 @@ async function buildSharePlans(run) {
|
|
|
2184
2201
|
if (rememberLinkWarning(run.linkWarnings, warning))
|
|
2185
2202
|
run.emit(brokenLinkSkipEvent(warning));
|
|
2186
2203
|
},
|
|
2204
|
+
onFilteredNamedPath: (namedPath, absolutePath, isDir) => recordIgnoredNamedPath(run, namedPath, absolutePath, isDir),
|
|
2187
2205
|
onLinkedSubtree: (rel) => run.linkedSubtreeSet.add(rel),
|
|
2188
2206
|
onHeartbeat: run.options.onHeartbeat,
|
|
2189
2207
|
cooperativeCheckpoint: run.options.cooperativeCheckpoint,
|
|
@@ -2200,7 +2218,7 @@ async function buildSharePlans(run) {
|
|
|
2200
2218
|
const fatal = collectFatalUnreachablePaths(run);
|
|
2201
2219
|
if (fatal.size > 0) {
|
|
2202
2220
|
emitUnreachablePathEvent(run);
|
|
2203
|
-
throw new UnreachablePushPathsError(fatal, run.syncRoot);
|
|
2221
|
+
throw new UnreachablePushPathsError(fatal, run.syncRoot, run.ignoredNamedPathRules);
|
|
2204
2222
|
}
|
|
2205
2223
|
const deleteScopeRoots = run.propagateDeletes === true
|
|
2206
2224
|
? resolveDeleteScopeRoots(run.paths, run.hqRoot, run.syncRoot, run.options.deleteScopeRoots)
|
|
@@ -2291,8 +2309,9 @@ async function buildSharePlans(run) {
|
|
|
2291
2309
|
companySlug: run.ctx.slug,
|
|
2292
2310
|
reportedUnsafeSymlinkPaths: run.reportedUnsafeSymlinkPaths,
|
|
2293
2311
|
});
|
|
2312
|
+
const recoveryHoldActive = run.holdRemoteDeletes || getJournalRecoveryMarker(run.journalSlug) !== undefined;
|
|
2294
2313
|
const deletePlan = run.propagateDeletes === true
|
|
2295
|
-
? await computeDeletePlan(run.journal, run.syncRoot, deleteScopeRoots, run.shouldSync, run.propagateDeletePolicy, run.ctx, run.options.personalMode !== true, run.skipSecretPaths, run.quarantinedPaths)
|
|
2314
|
+
? await computeDeletePlan(run.journal, run.syncRoot, deleteScopeRoots, run.shouldSync, run.propagateDeletePolicy, run.ctx, run.options.personalMode !== true, run.skipSecretPaths, run.quarantinedPaths, recoveryHoldActive, run.scopedJournal ? () => readJournalReadOnly(run.journalSlug) : undefined)
|
|
2296
2315
|
: { toDelete: [], toTombstone: [], refusedStale: [] };
|
|
2297
2316
|
for (const warning of deletePlan.brokenLinkSkipped ?? []) {
|
|
2298
2317
|
if (rememberLinkWarning(run.linkWarnings, warning))
|
|
@@ -3174,6 +3193,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3174
3193
|
if (outcome === "write-failed") {
|
|
3175
3194
|
// The merge already emitted the error. A second skip line repeats it.
|
|
3176
3195
|
hadUploadFailure = true;
|
|
3196
|
+
run.uploadFailedSet.add(relativePath);
|
|
3177
3197
|
}
|
|
3178
3198
|
counters.filesSkipped++;
|
|
3179
3199
|
return;
|
|
@@ -3232,6 +3252,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3232
3252
|
if (outcome === "write-failed") {
|
|
3233
3253
|
// The merge already emitted the error. A second skip line repeats it.
|
|
3234
3254
|
hadUploadFailure = true;
|
|
3255
|
+
run.uploadFailedSet.add(relativePath);
|
|
3235
3256
|
}
|
|
3236
3257
|
counters.filesSkipped++;
|
|
3237
3258
|
return;
|
|
@@ -3710,6 +3731,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3710
3731
|
if (outcome === "write-failed") {
|
|
3711
3732
|
// The merge already emitted the error. A second skip line repeats it.
|
|
3712
3733
|
hadUploadFailure = true;
|
|
3734
|
+
run.uploadFailedSet.add(relativePath);
|
|
3713
3735
|
}
|
|
3714
3736
|
else if (outcome === "publish-local") {
|
|
3715
3737
|
counters.filesSkippedConflict++;
|
|
@@ -3788,6 +3810,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3788
3810
|
return;
|
|
3789
3811
|
}
|
|
3790
3812
|
hadUploadFailure = true;
|
|
3813
|
+
run.uploadFailedSet.add(relativePath);
|
|
3791
3814
|
run.emit({
|
|
3792
3815
|
type: "error",
|
|
3793
3816
|
path: relativePath,
|
|
@@ -3890,6 +3913,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3890
3913
|
if (emitPlanLimitSkip(retryErr, relativePath))
|
|
3891
3914
|
return;
|
|
3892
3915
|
hadUploadFailure = true;
|
|
3916
|
+
run.uploadFailedSet.add(relativePath);
|
|
3893
3917
|
run.emit({
|
|
3894
3918
|
type: "error",
|
|
3895
3919
|
path: relativePath,
|
|
@@ -3939,6 +3963,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3939
3963
|
});
|
|
3940
3964
|
}
|
|
3941
3965
|
else {
|
|
3966
|
+
run.uploadFailedSet.add(relativePath);
|
|
3942
3967
|
run.emit({
|
|
3943
3968
|
type: "error",
|
|
3944
3969
|
path: relativePath,
|
|
@@ -5210,11 +5235,59 @@ function collectFatalUnreachablePaths(run) {
|
|
|
5210
5235
|
if (run.options.unreachablePathPolicy === "warn")
|
|
5211
5236
|
return fatal;
|
|
5212
5237
|
for (const [namedPath, reason] of run.unreachablePaths) {
|
|
5213
|
-
if (reason === "outside-company")
|
|
5238
|
+
if (reason === "outside-company" || reason === "ignored")
|
|
5214
5239
|
fatal.set(namedPath, reason);
|
|
5215
5240
|
}
|
|
5216
5241
|
return fatal;
|
|
5217
5242
|
}
|
|
5243
|
+
/**
|
|
5244
|
+
* A caller-named path that the ignore stack rejects was dropped with no event:
|
|
5245
|
+
* `.hq-*` names and generated INDEX.md files count as expected noise, so the
|
|
5246
|
+
* ignore warning skips them, and the push printed "✓ Pushed 0 file(s)" with
|
|
5247
|
+
* exit 0 (feedback_63bfc302, feedback_8c294805). Record it as `"ignored"` so
|
|
5248
|
+
* the default policy fails the push and names the rule.
|
|
5249
|
+
*
|
|
5250
|
+
* Not recorded:
|
|
5251
|
+
* - Runs without `failOnIgnoredNamedPaths`. Their paths are enumerated roots,
|
|
5252
|
+
* not user input, and an ignored root there is routine.
|
|
5253
|
+
* - `"warn"` runs. Their paths are watcher and walk roots, where an ignored
|
|
5254
|
+
* file is routine and already filtered.
|
|
5255
|
+
* - Paths the raw ignore stack admits. Scope, personal-vault, and secret-path
|
|
5256
|
+
* wrappers rejected those and report them through their own events.
|
|
5257
|
+
* - Secret-pattern files (dotenv files, credentials) and secret folders
|
|
5258
|
+
* (company `secrets/` and `settings/`, `.secrets`). They are left out by
|
|
5259
|
+
* design (US-178), and watcher runs name them routinely.
|
|
5260
|
+
*/
|
|
5261
|
+
function recordIgnoredNamedPath(run, namedPath, absolutePath, isDir) {
|
|
5262
|
+
if (run.options.failOnIgnoredNamedPaths !== true)
|
|
5263
|
+
return;
|
|
5264
|
+
if (run.options.unreachablePathPolicy === "warn")
|
|
5265
|
+
return;
|
|
5266
|
+
if (run.unreachablePaths.has(namedPath))
|
|
5267
|
+
return;
|
|
5268
|
+
if (run.ignoreFilter(absolutePath, isDir))
|
|
5269
|
+
return;
|
|
5270
|
+
const rel = vaultKeyForLocalPath(run.hqRoot, absolutePath);
|
|
5271
|
+
if (isSecretFolderPath(rel))
|
|
5272
|
+
return;
|
|
5273
|
+
if (!isDir && isSecretPatternIgnore(rel))
|
|
5274
|
+
return;
|
|
5275
|
+
run.unreachablePaths.set(namedPath, "ignored");
|
|
5276
|
+
run.ignoredNamedPathRules.set(namedPath, describeIgnoreRule(run, absolutePath, isDir));
|
|
5277
|
+
}
|
|
5278
|
+
function describeIgnoreRule(run, absolutePath, isDir) {
|
|
5279
|
+
if (!isDir && isHqGeneratedIndexFile(absolutePath)) {
|
|
5280
|
+
return "an HQ-generated INDEX.md (it starts with \"> Auto-generated.\"), which each machine rebuilds and sync never uploads";
|
|
5281
|
+
}
|
|
5282
|
+
const { layer } = run.explainIgnore(absolutePath, isDir);
|
|
5283
|
+
if (layer === ".hqignore" || layer === ".hqsyncignore") {
|
|
5284
|
+
return `excluded by a rule in ${layer}`;
|
|
5285
|
+
}
|
|
5286
|
+
if (layer === ".hqinclude allowlist") {
|
|
5287
|
+
return "not listed in .hqinclude";
|
|
5288
|
+
}
|
|
5289
|
+
return "excluded by a built-in sync ignore rule";
|
|
5290
|
+
}
|
|
5218
5291
|
/**
|
|
5219
5292
|
* Emit the `not-shipped` / `unreachable-path` event for a run, if any named
|
|
5220
5293
|
* path went unshipped. Shared by BOTH policies so the operator sees the same
|
|
@@ -5311,6 +5384,8 @@ function buildShareResult(run, counters, filesRefusedStalePaths, conflictPaths,
|
|
|
5311
5384
|
filesTombstoned: counters.filesTombstoned,
|
|
5312
5385
|
filesRefusedStale: counters.filesRefusedStale,
|
|
5313
5386
|
filesRefusedStalePaths,
|
|
5387
|
+
filesUploadFailed: run.uploadFailedSet.size,
|
|
5388
|
+
uploadFailedPaths: [...run.uploadFailedSet].slice(0, REFUSED_STALE_PATH_CAP),
|
|
5314
5389
|
filesSuppressedByTombstone: counters.filesSuppressedByTombstone,
|
|
5315
5390
|
...(run.skillRegisterLandedBytes
|
|
5316
5391
|
? { skillRegistrationFallbacks: run.skillRegistrationFallbacks }
|
|
@@ -5448,6 +5523,9 @@ function defaultConsoleLogger(event, humanSkipLines) {
|
|
|
5448
5523
|
console.error(` ⚠ cloud-placeholder, kept on remote: ${event.path} ` +
|
|
5449
5524
|
`(a .hqcloud placeholder stands in for this file, so it was not deleted)`);
|
|
5450
5525
|
}
|
|
5526
|
+
else if (event.reason === "state-rebuilt-deletes-held") {
|
|
5527
|
+
console.error(` ⚠ state-rebuilt-deletes-held, kept on remote: ${event.path} (complete a full authoritative sync pass to release the hold)`);
|
|
5528
|
+
}
|
|
5451
5529
|
else if (event.reason === "marker-mismatch") {
|
|
5452
5530
|
console.error(` ⚠ marker-mismatch, kept on remote: ${event.path} ` +
|
|
5453
5531
|
`(the remote object is not the symlink revision reindex replaced)`);
|
|
@@ -5895,8 +5973,10 @@ async function collectFiles(paths, hqRoot, syncRoot, filter, hooks = {}) {
|
|
|
5895
5973
|
// symlinks under allowlist mode (the motivating case for this
|
|
5896
5974
|
// whole branch). The filter is pure path lookup with no I/O,
|
|
5897
5975
|
// so two calls are free.
|
|
5898
|
-
if (!filter(absolutePath, false) && !filter(absolutePath, true))
|
|
5976
|
+
if (!filter(absolutePath, false) && !filter(absolutePath, true)) {
|
|
5977
|
+
hooks.onFilteredNamedPath?.(p, absolutePath, false);
|
|
5899
5978
|
continue;
|
|
5979
|
+
}
|
|
5900
5980
|
const target = readlinkOrNull(absolutePath);
|
|
5901
5981
|
if (target === null) {
|
|
5902
5982
|
console.error(` Warning: ${p} is an unreadable symbolic link, skipping.`);
|
|
@@ -5924,17 +6004,22 @@ async function collectFiles(paths, hqRoot, syncRoot, filter, hooks = {}) {
|
|
|
5924
6004
|
continue;
|
|
5925
6005
|
}
|
|
5926
6006
|
if (lstat.isDirectory()) {
|
|
5927
|
-
if (!filter(absolutePath, true))
|
|
6007
|
+
if (!filter(absolutePath, true)) {
|
|
6008
|
+
hooks.onFilteredNamedPath?.(p, absolutePath, true);
|
|
5928
6009
|
continue;
|
|
6010
|
+
}
|
|
5929
6011
|
await walkDir(absolutePath, syncRoot, filter, results, hooks, cooperativeWork);
|
|
5930
6012
|
}
|
|
5931
6013
|
else if (lstat.isFile()) {
|
|
5932
6014
|
const relativePath = vaultKeyForLocalPath(syncRoot, absolutePath);
|
|
6015
|
+
if (!filter(absolutePath)) {
|
|
6016
|
+
hooks.onFilteredNamedPath?.(p, absolutePath, false);
|
|
6017
|
+
continue;
|
|
6018
|
+
}
|
|
5933
6019
|
// Cloud-only placeholders are local stand-ins; never upload them,
|
|
5934
6020
|
// except library asset placeholders, which sync as small text files.
|
|
5935
|
-
if (
|
|
5936
|
-
(
|
|
5937
|
-
isSyncedCloudPlaceholderFile(absolutePath))) {
|
|
6021
|
+
if (!isCloudPlaceholderName(path.basename(absolutePath)) ||
|
|
6022
|
+
isSyncedCloudPlaceholderFile(absolutePath)) {
|
|
5938
6023
|
noteCooperativeRowVisit("collect", relativePath);
|
|
5939
6024
|
results.push({ kind: "file", absolutePath, relativePath });
|
|
5940
6025
|
}
|
|
@@ -6232,6 +6317,13 @@ function resolveDeleteScopeRoots(paths, hqRoot, syncRoot, explicitRoots = []) {
|
|
|
6232
6317
|
* already opts out of safety gates (currency check, owned-only).
|
|
6233
6318
|
* This guard honors that explicit opt-out.
|
|
6234
6319
|
*
|
|
6320
|
+
* `inScope` in a watcher (scoped) push is NOT the delete roots alone. The
|
|
6321
|
+
* watcher names each deleted key as its own delete root, so counting only
|
|
6322
|
+
* rows under those roots makes the ratio ~100% and refuses every batch of
|
|
6323
|
+
* MIN_ABS or more deletes, which the next pull then restores. When the local
|
|
6324
|
+
* ratio trips, a scoped push re-counts the denominator over the whole journal
|
|
6325
|
+
* (`bulkBaselineJournal`), the same rows a full pass's root would cover.
|
|
6326
|
+
*
|
|
6235
6327
|
* `"owned-only"` and `"currency-gated"` both go through the guard. The
|
|
6236
6328
|
* `direction === "up"` filter in `owned-only` is no defense once a user
|
|
6237
6329
|
* has ever run full bidirectional sync (every uploaded file gets
|
|
@@ -6388,7 +6480,10 @@ export async function computeDeletePlan(journal, syncRoot, scopeRoots, shouldSyn
|
|
|
6388
6480
|
// True for a COMPANY-scoped push (personalMode !== true). This distinguishes
|
|
6389
6481
|
// the canonical knowledge-marker layout from the personal-vault layout; it
|
|
6390
6482
|
// does not reinterpret ordinary vault-relative path segments.
|
|
6391
|
-
companyScoped = true, skipSecretPaths = false, excludedPaths = new Set()
|
|
6483
|
+
companyScoped = true, skipSecretPaths = false, excludedPaths = new Set(), holdRemoteDeletes = false,
|
|
6484
|
+
// Scoped (watcher) pushes only: reads the whole journal so the breaker's
|
|
6485
|
+
// denominator matches a full pass. Called only when the scoped ratio trips.
|
|
6486
|
+
bulkBaselineJournal) {
|
|
6392
6487
|
const plan = { toDelete: [], toTombstone: [], refusedStale: [] };
|
|
6393
6488
|
if (scopeRoots.length === 0)
|
|
6394
6489
|
return plan;
|
|
@@ -6517,6 +6612,15 @@ companyScoped = true, skipSecretPaths = false, excludedPaths = new Set()) {
|
|
|
6517
6612
|
continue;
|
|
6518
6613
|
}
|
|
6519
6614
|
}
|
|
6615
|
+
if (holdRemoteDeletes && !presentLocally) {
|
|
6616
|
+
plan.refusedStale.push({
|
|
6617
|
+
key: relativeKey,
|
|
6618
|
+
journalEtag: entry.remoteEtag ?? "<unknown>",
|
|
6619
|
+
remoteEtag: "<not-checked>",
|
|
6620
|
+
reason: "state-rebuilt-deletes-held",
|
|
6621
|
+
});
|
|
6622
|
+
continue;
|
|
6623
|
+
}
|
|
6520
6624
|
const materializedKnowledgeDir = presentLocally &&
|
|
6521
6625
|
isMaterializedKnowledgeDirectory(localPath, relativeKey, entry, companyScoped);
|
|
6522
6626
|
if (presentLocally && !materializedKnowledgeDir)
|
|
@@ -6770,11 +6874,30 @@ companyScoped = true, skipSecretPaths = false, excludedPaths = new Set()) {
|
|
|
6770
6874
|
// Bulk-asymmetry circuit-breaker. See `BULK_ASYMMETRY_*` constants for
|
|
6771
6875
|
// rationale + bypass paths. Skipped under policy:"all" (caller asserted
|
|
6772
6876
|
// intent) and under `HQ_SYNC_DELETE_BULK_OVERRIDE` (operator rollback knob).
|
|
6773
|
-
|
|
6877
|
+
const bulkRatioTrips = () => inScopeJournalEntries > 0 &&
|
|
6878
|
+
bulkCandidatePicks / inScopeJournalEntries >= BULK_ASYMMETRY_RATIO;
|
|
6879
|
+
const bulkGuardApplies = policy !== "all" &&
|
|
6774
6880
|
!isBulkAsymmetryOverride() &&
|
|
6775
|
-
bulkCandidatePicks >= BULK_ASYMMETRY_MIN_ABS
|
|
6776
|
-
|
|
6777
|
-
|
|
6881
|
+
bulkCandidatePicks >= BULK_ASYMMETRY_MIN_ABS;
|
|
6882
|
+
if (bulkGuardApplies && bulkRatioTrips() && bulkBaselineJournal) {
|
|
6883
|
+
// The rows a full pass would count, with the same hard filters as the
|
|
6884
|
+
// Stage-1 loop. Rows it could not lstat (broken links) are not removed,
|
|
6885
|
+
// so this can only exceed a full pass's denominator by those rows.
|
|
6886
|
+
let baseline = 0;
|
|
6887
|
+
for (const [key, entry] of journalRows(bulkBaselineJournal()).entries()) {
|
|
6888
|
+
if (excludedPaths.has(key))
|
|
6889
|
+
continue;
|
|
6890
|
+
if (isNeverSyncedVaultPath(key))
|
|
6891
|
+
continue;
|
|
6892
|
+
if (skipSecretPaths && isSecretBearingVaultPath(key))
|
|
6893
|
+
continue;
|
|
6894
|
+
if (isTombstone(entry) && entry.removedReason !== "local-delete")
|
|
6895
|
+
continue;
|
|
6896
|
+
baseline++;
|
|
6897
|
+
}
|
|
6898
|
+
inScopeJournalEntries = Math.max(inScopeJournalEntries, baseline);
|
|
6899
|
+
}
|
|
6900
|
+
if (bulkGuardApplies && bulkRatioTrips()) {
|
|
6778
6901
|
// Move every staged candidate (both already-bucketed toDelete from
|
|
6779
6902
|
// owned-only and queued headCandidates from currency-gated) into
|
|
6780
6903
|
// refusedStale with reason "bulk-asymmetry", and re-label the intent-less
|