@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/cli/share.js
CHANGED
|
@@ -8,7 +8,7 @@ import * as fs from "fs";
|
|
|
8
8
|
import * as os from "os";
|
|
9
9
|
import * as path from "path";
|
|
10
10
|
import { resolveSymlinkTargetRefusalFlag } from "../sync/symlink-target-refusal-flag.js";
|
|
11
|
-
import { inspectSymlinkTarget, materializedTargetMatchesWire, personalSymlinkTargetLinkPath, } from "../symlink-target-policy.js";
|
|
11
|
+
import { inspectLocalSymlinkScope, inspectSymlinkTarget, materializedTargetMatchesWire, personalSymlinkTargetLinkPath, } from "../symlink-target-policy.js";
|
|
12
12
|
import { resolveEntityContext, isExpiringSoon, refreshEntityContext } from "../context.js";
|
|
13
13
|
import { ensureCompanyVaultTransport } from "../company-vault-transport.js";
|
|
14
14
|
import { createSyncProgressRecorder } from "../sync-progress.js";
|
|
@@ -17,7 +17,7 @@ import { uploadFile, uploadSymlink, toPosixKey, headRemoteFile, readVerifiedRemo
|
|
|
17
17
|
import * as crypto from "crypto";
|
|
18
18
|
import { bindObjectIOToContext, PresignObjectIO, RateLimitedError, resolveObjectIO, S3SdkObjectIO, } from "../object-io.js";
|
|
19
19
|
import { primePrivateFolderUpload } from "../private-folder-transport.js";
|
|
20
|
-
import { isUnderAnyRoot, readJournalForPass, readJournalScoped, writeJournal, setJournalSnapshotQuarantineListener, hashFile, hashSymlinkTarget, journalRows, journalEntryMatchesStat, journalFingerprintMatchesStat, dropJournalDecodeWindow, timestampsTooCoarseToTrust, updateEntry, PrematureJournalEntryError, removeEntry, isTombstone, normalizeEtag, PERSONAL_VAULT_JOURNAL_SLUG, migratePersonalVaultJournal, migrateToV2, } from "../journal.js";
|
|
20
|
+
import { 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";
|
|
21
21
|
import { createIgnoreFilter, hasControlCharacters, isExpectedIgnore, isSecretPatternIgnore, isWithinSizeLimit, } from "../ignore.js";
|
|
22
22
|
import { wrapFilterWithPersonalVaultDefaults, } from "../personal-vault-exclusions.js";
|
|
23
23
|
import { isSecretBearingVaultPath, isSkipSecretPathsEnabled, } from "../sync/secret-path-policy.js";
|
|
@@ -29,7 +29,7 @@ import { compileScopeMatcher, } from "../prefix-coalesce.js";
|
|
|
29
29
|
import { hasRemoteChanged, isAccessDenied, resolveActiveCompany, resolveTransferConcurrency, resolveUploadByteBudget, readHostLoad, } from "../sync-core.js";
|
|
30
30
|
import { buildConflictBackupPath, buildConflictId, buildConflictProbePath, readShortMachineId, } from "../lib/conflict-file.js";
|
|
31
31
|
import { appendConflictEntry } from "../lib/conflict-index.js";
|
|
32
|
-
import {
|
|
32
|
+
import { isLocallyIneligibleForPush } from "../lib/cloud-authoritative.js";
|
|
33
33
|
import { isOntologyEntityKey, ONTOLOGY_MULTI_WRITER_FLAG, resolveOntologyEntityPush, } from "../lib/ontology-entity-push.js";
|
|
34
34
|
import { entityIsFreshForCreate, outboxAllowsEntityPush, readOntologyOutbox, } from "../lib/ontology-outbox.js";
|
|
35
35
|
import { resolveSkillRegisterLandedBytes } from "../lib/skill-register-landed-bytes.js";
|
|
@@ -447,7 +447,7 @@ function refusesServerOwnedPush(relativePath, ontologyEntityPush, absolutePath,
|
|
|
447
447
|
return false;
|
|
448
448
|
return !outboxAllowsEntityPush(ontologyEntityPush, relativePath, absolutePath, plannedHash);
|
|
449
449
|
}
|
|
450
|
-
return
|
|
450
|
+
return isLocallyIneligibleForPush(relativePath, { boardClientOwned: true });
|
|
451
451
|
}
|
|
452
452
|
function computePushPlan(filesToShare, journal, skipUnchanged, onHeartbeat, ontologyEntityPush = false, symlinkContext) {
|
|
453
453
|
const items = [];
|
|
@@ -658,6 +658,22 @@ function portablePushSymlinkTarget(absolutePath, relativePath, localTarget, wire
|
|
|
658
658
|
linkPath = `companies/${context.companySlug}/${relativePath}`;
|
|
659
659
|
}
|
|
660
660
|
}
|
|
661
|
+
if (scope && scopeRoot) {
|
|
662
|
+
const localInspection = inspectLocalSymlinkScope({
|
|
663
|
+
absoluteLinkPath: absolutePath,
|
|
664
|
+
linkPath,
|
|
665
|
+
scopeRoot,
|
|
666
|
+
scope,
|
|
667
|
+
});
|
|
668
|
+
if (localInspection.refused) {
|
|
669
|
+
const event = {
|
|
670
|
+
classification: localInspection.classification,
|
|
671
|
+
shape: localInspection.shape,
|
|
672
|
+
};
|
|
673
|
+
reportRefusal(event);
|
|
674
|
+
return { target: "", refused: true, ...event };
|
|
675
|
+
}
|
|
676
|
+
}
|
|
661
677
|
const reuseMatchingWireTarget = () => {
|
|
662
678
|
if (wireTarget === undefined || !scope || !materializedTargetMatchesWire(absolutePath, localTarget, wireTarget)) {
|
|
663
679
|
return undefined;
|
|
@@ -988,6 +1004,17 @@ async function shareOnce(options) {
|
|
|
988
1004
|
const conflictPaths = [];
|
|
989
1005
|
const pathResults = [];
|
|
990
1006
|
const plans = await buildSharePlans(run);
|
|
1007
|
+
if (options.preflightOnly === true) {
|
|
1008
|
+
const result = buildShareResult(run, counters, filesRefusedStalePaths, conflictPaths, pathResults, false);
|
|
1009
|
+
result.preflightCounts = {
|
|
1010
|
+
pushes: plans.pushPlan.filesToUpload,
|
|
1011
|
+
remoteDeletes: plans.deletePlan.toDelete.length + plans.decommissionPlan.length,
|
|
1012
|
+
};
|
|
1013
|
+
return result;
|
|
1014
|
+
}
|
|
1015
|
+
if (run.quarantinedPaths.size > 0) {
|
|
1016
|
+
run.emit({ type: "journal-quarantine-unresolved", count: run.quarantinedPaths.size });
|
|
1017
|
+
}
|
|
991
1018
|
const serverOwnedRefusals = plans.pushPlan.items.filter((item) => item.action === "refuse-server-owned");
|
|
992
1019
|
if (serverOwnedRefusals.length > 0 &&
|
|
993
1020
|
serverOwnedRefusals.length === plans.pushPlan.items.length &&
|
|
@@ -1261,6 +1288,7 @@ async function createPushRunContext(options) {
|
|
|
1261
1288
|
? wrapFilterWithScope(transferFilter, syncRoot, options.prefixSet, onScopeExcluded)
|
|
1262
1289
|
: transferFilter;
|
|
1263
1290
|
const journalSlug = options.journalSlug ?? ctx.slug;
|
|
1291
|
+
const quarantinedPaths = readQuarantinedJournalPaths(journalSlug);
|
|
1264
1292
|
const flagConfig = vaultConfig ?? privateFolderVaultConfig;
|
|
1265
1293
|
const symlinkTargetRefusal = flagConfig
|
|
1266
1294
|
? await resolveSymlinkTargetRefusalFlag({
|
|
@@ -1275,7 +1303,7 @@ async function createPushRunContext(options) {
|
|
|
1275
1303
|
// defer its keyed journal read until the exact K upload/delete keys are
|
|
1276
1304
|
// known. Full/cadence callers retain the complete snapshot here.
|
|
1277
1305
|
const scopedJournal = options.scopedJournal === true;
|
|
1278
|
-
const journal = scopedJournal ? emptyJournalForScopedPass() : readJournalForPass(journalSlug);
|
|
1306
|
+
const journal = scopedJournal ? emptyJournalForScopedPass() : readJournalForPass(journalSlug, ctx.uid);
|
|
1279
1307
|
return {
|
|
1280
1308
|
options,
|
|
1281
1309
|
ontologyEntityPush: false,
|
|
@@ -1303,6 +1331,7 @@ async function createPushRunContext(options) {
|
|
|
1303
1331
|
durableShouldSync: baseFilter,
|
|
1304
1332
|
skipSecretPaths,
|
|
1305
1333
|
journalSlug,
|
|
1334
|
+
quarantinedPaths,
|
|
1306
1335
|
journal,
|
|
1307
1336
|
scopedJournal,
|
|
1308
1337
|
excludedSet,
|
|
@@ -1967,7 +1996,7 @@ async function reconcileLandedCompanySkill(run, relativePath, content, absoluteP
|
|
|
1967
1996
|
}
|
|
1968
1997
|
}
|
|
1969
1998
|
async function buildSharePlans(run) {
|
|
1970
|
-
const
|
|
1999
|
+
const collectedAll = await collectFiles(run.paths, run.hqRoot, run.syncRoot, run.shouldSync, {
|
|
1971
2000
|
onUnreachablePath: (namedPath, reason) => {
|
|
1972
2001
|
// First reason wins: a path is named once, and re-adding would only
|
|
1973
2002
|
// churn the map ordering the error message and event sample rely on.
|
|
@@ -1979,6 +2008,7 @@ async function buildSharePlans(run) {
|
|
|
1979
2008
|
onHeartbeat: run.options.onHeartbeat,
|
|
1980
2009
|
cooperativeCheckpoint: run.options.cooperativeCheckpoint,
|
|
1981
2010
|
});
|
|
2011
|
+
const collected = collectedAll.filter((entry) => !run.quarantinedPaths.has(entry.relativePath));
|
|
1982
2012
|
// Ask #2 of feedback_a51cb63d: "error, not warn-skip, when the named file
|
|
1983
2013
|
// exists locally but is unreachable by the resolver". This is the throw that
|
|
1984
2014
|
// makes it true end-to-end — the CLI's push handler already turns a thrown
|
|
@@ -1995,7 +2025,7 @@ async function buildSharePlans(run) {
|
|
|
1995
2025
|
const deleteScopeRoots = run.propagateDeletes === true
|
|
1996
2026
|
? resolveDeleteScopeRoots(run.paths, run.hqRoot, run.syncRoot, run.options.deleteScopeRoots)
|
|
1997
2027
|
: [];
|
|
1998
|
-
await refreshPushJournalAfterWalk(run, collected.map((entry) => entry.relativePath), deleteScopeRoots);
|
|
2028
|
+
await refreshPushJournalAfterWalk(run, [...collected.map((entry) => entry.relativePath), ...run.quarantinedPaths], deleteScopeRoots);
|
|
1999
2029
|
// Control-character key filter (macOS Finder Icon\r incident). A key
|
|
2000
2030
|
// containing a control character can never be stored — validateVaultUploadKey
|
|
2001
2031
|
// rejects it with INVALID_KEY_CONTROL_CHARS — so planning an upload for one
|
|
@@ -2070,6 +2100,9 @@ async function buildSharePlans(run) {
|
|
|
2070
2100
|
// push, so ordinary passes make no flag request.
|
|
2071
2101
|
if (pushEntries.some((entry) => isOntologyEntityKey(entry.relativePath))) {
|
|
2072
2102
|
run.ontologyEntityPush = await resolveRunOntologyEntityPush(run);
|
|
2103
|
+
if (run.options.personalMode !== true) {
|
|
2104
|
+
run.options.onCatchupOntologyPush?.(run.ctx.slug, run.ontologyEntityPush !== false);
|
|
2105
|
+
}
|
|
2073
2106
|
}
|
|
2074
2107
|
const pushPlan = await computePushPlanCooperatively(pushEntries, run.journal, run.skipUnchanged === true, run.options.onHeartbeat, run.options.cooperativeCheckpoint, run.ontologyEntityPush, {
|
|
2075
2108
|
hqRoot: run.hqRoot,
|
|
@@ -2079,11 +2112,15 @@ async function buildSharePlans(run) {
|
|
|
2079
2112
|
reportedUnsafeSymlinkPaths: run.reportedUnsafeSymlinkPaths,
|
|
2080
2113
|
});
|
|
2081
2114
|
const deletePlan = run.propagateDeletes === true
|
|
2082
|
-
? await computeDeletePlan(run.journal, run.syncRoot, deleteScopeRoots, run.shouldSync, run.propagateDeletePolicy, run.ctx, run.options.personalMode !== true, run.skipSecretPaths)
|
|
2115
|
+
? await computeDeletePlan(run.journal, run.syncRoot, deleteScopeRoots, run.shouldSync, run.propagateDeletePolicy, run.ctx, run.options.personalMode !== true, run.skipSecretPaths, run.quarantinedPaths)
|
|
2083
2116
|
: { toDelete: [], toTombstone: [], refusedStale: [] };
|
|
2084
2117
|
const decommissionPlan = !run.scopedJournal && (run.options.decommissionPrefixes ?? []).length > 0
|
|
2085
2118
|
? computeDecommissionPlan(run.journal, run.options.decommissionPrefixes ?? [], run.propagateDeletePolicy, new Set([...deletePlan.toDelete.map((item) => item.key), ...deletePlan.toTombstone]), run.skipSecretPaths)
|
|
2086
2119
|
: [];
|
|
2120
|
+
for (let index = decommissionPlan.length - 1; index >= 0; index--) {
|
|
2121
|
+
if (run.quarantinedPaths.has(decommissionPlan[index]))
|
|
2122
|
+
decommissionPlan.splice(index, 1);
|
|
2123
|
+
}
|
|
2087
2124
|
const remoteDeleteKeys = [
|
|
2088
2125
|
...deletePlan.toDelete.map((item) => item.key),
|
|
2089
2126
|
...decommissionPlan,
|
|
@@ -2179,7 +2216,7 @@ async function refreshPushJournalAfterWalk(run, collectedKeys = [], deleteScopeR
|
|
|
2179
2216
|
}
|
|
2180
2217
|
run.journal = migrateToV2(run.scopedJournal
|
|
2181
2218
|
? readJournalScoped(run.journalSlug, collectedKeys, deleteScopeRoots)
|
|
2182
|
-
: readJournalForPass(run.journalSlug));
|
|
2219
|
+
: readJournalForPass(run.journalSlug, run.ctx.uid));
|
|
2183
2220
|
run.checkpoint.baselineLastSync = run.journal.lastSync;
|
|
2184
2221
|
run.checkpoint.threshold = checkpointThresholdFor(run.journal, run.options.durableFullPassProgress === true);
|
|
2185
2222
|
// Purge classifies every journal row and decommission plans likewise scan
|
|
@@ -2334,13 +2371,17 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2334
2371
|
hadUploadFailure: false,
|
|
2335
2372
|
};
|
|
2336
2373
|
}
|
|
2337
|
-
await primeUploads(run.ctx, initiallyPlannedUploads.map((it) =>
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2374
|
+
await primeUploads(run.ctx, initiallyPlannedUploads.map((it) => {
|
|
2375
|
+
const baselineEtag = journalRows(run.journal).get(it.relativePath)?.remoteEtag;
|
|
2376
|
+
return {
|
|
2377
|
+
key: it.relativePath,
|
|
2378
|
+
localPath: it.absolutePath,
|
|
2379
|
+
isSymlink: it.kind === "symlink",
|
|
2380
|
+
...(it.kind === "file" ? { contentHash: it.localHash } : {}),
|
|
2381
|
+
...(baselineEtag ? { ifMatch: baselineEtag } : {}),
|
|
2382
|
+
author: run.options.author,
|
|
2383
|
+
};
|
|
2384
|
+
}));
|
|
2344
2385
|
// Warm the GET presigns the per-item conflict HEAD (remoteMeta) reuses, so a
|
|
2345
2386
|
// large upload set doesn't mint one presign per HEAD and burst/trip the
|
|
2346
2387
|
// presign breaker. Mirrors the new-files + tombstone pre-primes on the pull.
|
|
@@ -2401,8 +2442,38 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2401
2442
|
}
|
|
2402
2443
|
planLimitPauses.save(now);
|
|
2403
2444
|
};
|
|
2445
|
+
let catchupSkipStatWarningReported = false;
|
|
2446
|
+
const recordCatchupSkip = (relativePath, absolutePath, reason) => {
|
|
2447
|
+
const record = run.options.onCatchupSkip;
|
|
2448
|
+
if (!record)
|
|
2449
|
+
return;
|
|
2450
|
+
try {
|
|
2451
|
+
const stat = fs.lstatSync(absolutePath);
|
|
2452
|
+
if (!stat.isFile() && !stat.isSymbolicLink())
|
|
2453
|
+
return;
|
|
2454
|
+
record({
|
|
2455
|
+
company: run.options.personalMode === true ? "personal" : run.ctx.slug,
|
|
2456
|
+
path: relativePath,
|
|
2457
|
+
reason,
|
|
2458
|
+
size: stat.size,
|
|
2459
|
+
mtimeMs: stat.mtimeMs,
|
|
2460
|
+
...(run.options.catchupGrantVersion
|
|
2461
|
+
? { grantVersion: run.options.catchupGrantVersion }
|
|
2462
|
+
: {}),
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
catch (error) {
|
|
2466
|
+
// A path that vanished or became unreadable cannot suppress a later warm-up candidate.
|
|
2467
|
+
if (!catchupSkipStatWarningReported) {
|
|
2468
|
+
catchupSkipStatWarningReported = true;
|
|
2469
|
+
process.stderr.write(`hq-cloud: catch-up skip metadata unavailable; leaving path eligible: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
};
|
|
2404
2473
|
const settleFreshSkip = (item) => {
|
|
2405
2474
|
if (item.action === "refuse-server-owned") {
|
|
2475
|
+
// Server-owned paths share the local cloud-authoritative predicate with
|
|
2476
|
+
// warm-up. They never enter the host-local remote-refusal skip store.
|
|
2406
2477
|
if (run.options.serverOwnedPathPolicy !== "skip") {
|
|
2407
2478
|
emitServerOwnedRefusal(run, item.relativePath);
|
|
2408
2479
|
}
|
|
@@ -2496,8 +2567,10 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2496
2567
|
// than for the `localDiverges` recovery. The two cases need different write
|
|
2497
2568
|
// fences — see the precondition block below.
|
|
2498
2569
|
let fenceOnObservedRemote = false;
|
|
2570
|
+
let remoteChangedSinceJournal = false;
|
|
2499
2571
|
// Cloud-authoritative paths (server-regenerated: company-brief.md, board.json,
|
|
2500
|
-
// ontology
|
|
2572
|
+
// pipeline-owned ontology/, signals/, and sources/) are PULL-WINS and server-owned.
|
|
2573
|
+
// The push leg must
|
|
2501
2574
|
// NEVER upload the local copy over them — not just on a two-sided conflict, but on
|
|
2502
2575
|
// ANY divergence, including a one-sided local change. A stale local mirror (e.g. a
|
|
2503
2576
|
// second HQ root that shares this machine's sync journal) would otherwise read as
|
|
@@ -2528,6 +2601,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2528
2601
|
catch (headErr) {
|
|
2529
2602
|
if (isAccessDenied(headErr)) {
|
|
2530
2603
|
run.scopeExcludedSet.add(relativePath);
|
|
2604
|
+
recordCatchupSkip(relativePath, absolutePath, "access-denied");
|
|
2531
2605
|
return;
|
|
2532
2606
|
}
|
|
2533
2607
|
throw headErr;
|
|
@@ -2539,6 +2613,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2539
2613
|
if (remoteMeta &&
|
|
2540
2614
|
!journalRows(run.journal).has(relativePath) &&
|
|
2541
2615
|
isArchivedSessionLog(relativePath, remoteMeta)) {
|
|
2616
|
+
recordCatchupSkip(relativePath, absolutePath, "archived-session-log");
|
|
2542
2617
|
run.emit({ type: "skip-archived-session-log", path: relativePath });
|
|
2543
2618
|
counters.filesSkipped++;
|
|
2544
2619
|
return;
|
|
@@ -2634,6 +2709,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2634
2709
|
const remoteContentHash = item.kind === "file" ? publishedContentHash(remoteMeta.metadata) : undefined;
|
|
2635
2710
|
const remoteChanged = !!journalEntry &&
|
|
2636
2711
|
hasRemoteChanged({ ...remoteMeta, contentHash: remoteContentHash }, journalEntry);
|
|
2712
|
+
remoteChangedSinceJournal = remoteChanged;
|
|
2637
2713
|
// A matching published digest is a content no-op even when SSE-KMS gave
|
|
2638
2714
|
// this object a fresh ETag. Reconcile the journal to that version without
|
|
2639
2715
|
// a needless PUT or a phantom two-sided conflict.
|
|
@@ -2677,6 +2753,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2677
2753
|
// ordinary journaled edit whose remote is unchanged still uploads.
|
|
2678
2754
|
const needsRemoteBody = !journalEntry || (localChanged && remoteChanged) || !!journalEntry.localDiverges;
|
|
2679
2755
|
if (needsRemoteBody && isArchivedSessionLog(relativePath, remoteMeta)) {
|
|
2756
|
+
recordCatchupSkip(relativePath, absolutePath, "archived-session-log");
|
|
2680
2757
|
run.emit({ type: "skip-archived-session-log", path: relativePath });
|
|
2681
2758
|
counters.filesSkipped++;
|
|
2682
2759
|
return;
|
|
@@ -2802,7 +2879,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
2802
2879
|
let winnerProbe;
|
|
2803
2880
|
let resolution;
|
|
2804
2881
|
if (versionAware) {
|
|
2805
|
-
winnerProbe = await probeRemoteForWinner(run, item, remoteMeta.lastModified);
|
|
2882
|
+
winnerProbe = await probeRemoteForWinner(run, item, remoteMeta.lastModified, remoteChangedSinceJournal);
|
|
2806
2883
|
resolution = "keep";
|
|
2807
2884
|
}
|
|
2808
2885
|
else {
|
|
@@ -3069,6 +3146,11 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3069
3146
|
settleLandedReconcile(run, relativePath, outcome);
|
|
3070
3147
|
checkpointShareJournal(run);
|
|
3071
3148
|
}
|
|
3149
|
+
run.options.onAuditPathEvent?.({
|
|
3150
|
+
path: relativePath,
|
|
3151
|
+
contentHash: localHash,
|
|
3152
|
+
bytes: size,
|
|
3153
|
+
});
|
|
3072
3154
|
if (alreadyCurrent) {
|
|
3073
3155
|
run.emit({ type: "reconciled", path: relativePath, direction: "push" });
|
|
3074
3156
|
}
|
|
@@ -3127,7 +3209,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
3127
3209
|
// pass starts over); remote winning adopts it exactly as the
|
|
3128
3210
|
// pre-flight conflict path would have.
|
|
3129
3211
|
const latest = await headRemoteFile(run.ctx, relativePath);
|
|
3130
|
-
const probe = await probeRemoteForWinner(run, item, latest?.lastModified ?? null);
|
|
3212
|
+
const probe = await probeRemoteForWinner(run, item, latest?.lastModified ?? null, true);
|
|
3131
3213
|
const decision = probe.decision;
|
|
3132
3214
|
run.emit({
|
|
3133
3215
|
type: "conflict",
|
|
@@ -3511,7 +3593,7 @@ function isArchivedSessionLog(relativePath, remoteMeta) {
|
|
|
3511
3593
|
* either side, probe failure); the caller then falls back to the historical
|
|
3512
3594
|
* `keep` outcome (adopt the remote). Session-level auth refusals propagate.
|
|
3513
3595
|
*/
|
|
3514
|
-
async function probeRemoteForWinner(run, item, remoteMtime) {
|
|
3596
|
+
async function probeRemoteForWinner(run, item, remoteMtime, remoteChangedSinceJournal = false) {
|
|
3515
3597
|
const probeAbs = buildConflictProbePath(run.hqRoot);
|
|
3516
3598
|
let parked = false;
|
|
3517
3599
|
let fetched;
|
|
@@ -3565,6 +3647,7 @@ async function probeRemoteForWinner(run, item, remoteMtime) {
|
|
|
3565
3647
|
remoteBytes: fs.readFileSync(probeAbs),
|
|
3566
3648
|
localMtime: localStat.mtime,
|
|
3567
3649
|
remoteMtime,
|
|
3650
|
+
remoteChangedSinceJournal,
|
|
3568
3651
|
});
|
|
3569
3652
|
}
|
|
3570
3653
|
}
|
|
@@ -3871,6 +3954,12 @@ async function executeDeletes(run, deletePlan, decommissionPlan, counters, files
|
|
|
3871
3954
|
await deleteRemoteFile(run.ctx, relativePath);
|
|
3872
3955
|
removeEntry(run.journal, relativePath);
|
|
3873
3956
|
counters.filesDeleted++;
|
|
3957
|
+
run.options.onAuditPathEvent?.({
|
|
3958
|
+
path: relativePath,
|
|
3959
|
+
...(entry?.remoteEtag ? { etag: entry.remoteEtag } : {}),
|
|
3960
|
+
bytes: size,
|
|
3961
|
+
deleted: true,
|
|
3962
|
+
});
|
|
3874
3963
|
run.emit({
|
|
3875
3964
|
type: "progress",
|
|
3876
3965
|
path: relativePath,
|
|
@@ -3941,9 +4030,9 @@ async function executeDeletes(run, deletePlan, decommissionPlan, counters, files
|
|
|
3941
4030
|
}
|
|
3942
4031
|
for (const relativePath of deletePlan.toTombstone) {
|
|
3943
4032
|
const localPath = localPathForVaultKey(run.syncRoot, relativePath);
|
|
4033
|
+
const entry = journalRows(run.journal).get(relativePath);
|
|
3944
4034
|
try {
|
|
3945
4035
|
const lstat = fs.lstatSync(localPath);
|
|
3946
|
-
const entry = journalRows(run.journal).get(relativePath);
|
|
3947
4036
|
if (lstat.isFile()) {
|
|
3948
4037
|
const localHash = hashFile(localPath);
|
|
3949
4038
|
if (entry?.hash && entry.hash !== localHash) {
|
|
@@ -3994,6 +4083,12 @@ async function executeDeletes(run, deletePlan, decommissionPlan, counters, files
|
|
|
3994
4083
|
}
|
|
3995
4084
|
removeEntry(run.journal, relativePath);
|
|
3996
4085
|
counters.filesTombstoned++;
|
|
4086
|
+
run.options.onAuditPathEvent?.({
|
|
4087
|
+
path: relativePath,
|
|
4088
|
+
...(entry?.remoteEtag ? { etag: entry.remoteEtag } : {}),
|
|
4089
|
+
bytes: 0,
|
|
4090
|
+
deleted: true,
|
|
4091
|
+
});
|
|
3997
4092
|
run.emit({
|
|
3998
4093
|
type: "progress",
|
|
3999
4094
|
path: relativePath,
|
|
@@ -5327,7 +5422,7 @@ async function computeDeletePlan(journal, syncRoot, scopeRoots, shouldSync, poli
|
|
|
5327
5422
|
// True for a COMPANY-scoped push (personalMode !== true). This distinguishes
|
|
5328
5423
|
// the canonical knowledge-marker layout from the personal-vault layout; it
|
|
5329
5424
|
// does not reinterpret ordinary vault-relative path segments.
|
|
5330
|
-
companyScoped = true, skipSecretPaths = false) {
|
|
5425
|
+
companyScoped = true, skipSecretPaths = false, excludedPaths = new Set()) {
|
|
5331
5426
|
const plan = { toDelete: [], toTombstone: [], refusedStale: [] };
|
|
5332
5427
|
if (scopeRoots.length === 0)
|
|
5333
5428
|
return plan;
|
|
@@ -5369,6 +5464,8 @@ companyScoped = true, skipSecretPaths = false) {
|
|
|
5369
5464
|
const scopeRootSet = new Set(scopeRoots);
|
|
5370
5465
|
const everyRowInScope = scopeRootSet.has("");
|
|
5371
5466
|
for (const [relativeKey, entry] of journalRows(journal).entries()) {
|
|
5467
|
+
if (excludedPaths.has(relativeKey))
|
|
5468
|
+
continue;
|
|
5372
5469
|
const inScope = everyRowInScope || isUnderAnyRoot(relativeKey, scopeRootSet);
|
|
5373
5470
|
if (!inScope)
|
|
5374
5471
|
continue;
|