@indigoai-us/hq-cloud 6.15.30 → 6.15.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +24 -13
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +0 -71
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +0 -10
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +13 -170
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +1 -16
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +2 -40
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.test.js +1 -53
- package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +3 -15
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +13 -66
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +13 -433
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/conflict.d.ts +2 -25
- package/dist/cli/conflict.d.ts.map +1 -1
- package/dist/cli/conflict.js +4 -29
- package/dist/cli/conflict.js.map +1 -1
- package/dist/cli/share.d.ts +4 -40
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +86 -478
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +150 -845
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync-scope.test.js +18 -147
- package/dist/cli/sync-scope.test.js.map +1 -1
- package/dist/cli/sync.d.ts +0 -20
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +29 -261
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +90 -519
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/journal.d.ts +0 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +25 -155
- package/dist/journal.js.map +1 -1
- package/dist/journal.test.js +11 -29
- package/dist/journal.test.js.map +1 -1
- package/dist/object-io.d.ts +13 -34
- package/dist/object-io.d.ts.map +1 -1
- package/dist/object-io.js +29 -154
- package/dist/object-io.js.map +1 -1
- package/dist/outcome-telemetry.d.ts +1 -7
- package/dist/outcome-telemetry.d.ts.map +1 -1
- package/dist/outcome-telemetry.js +9 -15
- package/dist/outcome-telemetry.js.map +1 -1
- package/dist/outcome-telemetry.test.js +0 -15
- package/dist/outcome-telemetry.test.js.map +1 -1
- package/dist/outposts/client.d.ts +2 -2
- package/dist/outposts/client.d.ts.map +1 -1
- package/dist/outposts/client.js +3 -4
- package/dist/outposts/client.js.map +1 -1
- package/dist/outposts/command.d.ts.map +1 -1
- package/dist/outposts/command.js +6 -9
- package/dist/outposts/command.js.map +1 -1
- package/dist/outposts/command.test.js +2 -59
- package/dist/outposts/command.test.js.map +1 -1
- package/dist/outposts/remote-command.d.ts +8 -8
- package/dist/outposts/remote-command.d.ts.map +1 -1
- package/dist/outposts/remote-command.js +8 -10
- package/dist/outposts/remote-command.js.map +1 -1
- package/dist/prefix-coalesce.d.ts +0 -14
- package/dist/prefix-coalesce.d.ts.map +1 -1
- package/dist/prefix-coalesce.js +0 -29
- package/dist/prefix-coalesce.js.map +1 -1
- package/dist/prefix-coalesce.test.js +1 -22
- package/dist/prefix-coalesce.test.js.map +1 -1
- package/dist/skill-telemetry.d.ts +1 -6
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +8 -14
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +0 -43
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/sts-credential-refresh.test.js +2 -87
- package/dist/sts-credential-refresh.test.js.map +1 -1
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +1 -20
- package/dist/sync/state-store.js.map +1 -1
- package/dist/sync/state-store.test.js +0 -64
- package/dist/sync/state-store.test.js.map +1 -1
- package/dist/sync-core.d.ts.map +1 -1
- package/dist/sync-core.js +2 -7
- package/dist/sync-core.js.map +1 -1
- package/dist/sync-core.test.js +0 -36
- package/dist/sync-core.test.js.map +1 -1
- package/dist/telemetry.d.ts +6 -30
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +135 -331
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +0 -403
- package/dist/telemetry.test.js.map +1 -1
- package/dist/types.d.ts +3 -26
- package/dist/types.d.ts.map +1 -1
- package/dist/watcher.d.ts +0 -76
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +3 -105
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +1 -165
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
- package/dist/bandwidth.d.ts +0 -128
- package/dist/bandwidth.d.ts.map +0 -1
- package/dist/bandwidth.js +0 -254
- package/dist/bandwidth.js.map +0 -1
- package/dist/bandwidth.test.d.ts +0 -2
- package/dist/bandwidth.test.d.ts.map +0 -1
- package/dist/bandwidth.test.js +0 -243
- package/dist/bandwidth.test.js.map +0 -1
- package/dist/journal-delta-writes.test.d.ts +0 -2
- package/dist/journal-delta-writes.test.d.ts.map +0 -1
- package/dist/journal-delta-writes.test.js +0 -140
- package/dist/journal-delta-writes.test.js.map +0 -1
package/dist/cli/sync.js
CHANGED
|
@@ -8,7 +8,6 @@ import * as fs from "fs";
|
|
|
8
8
|
import * as path from "path";
|
|
9
9
|
import { VaultAuthError, VaultClient } from "../vault-client.js";
|
|
10
10
|
import { readlinkOrNull } from "../lib/readlink-safe.js";
|
|
11
|
-
import { describeError } from "../lib/describe-error.js";
|
|
12
11
|
import { emitCloudTelemetry, } from "../telemetry-events.js";
|
|
13
12
|
import { resolveEntityContext, isExpiringSoon, refreshEntityContext } from "../context.js";
|
|
14
13
|
import { createSyncProgressRecorder } from "../sync-progress.js";
|
|
@@ -19,8 +18,7 @@ import { readJournal, writeJournal, hashFile, hashSymlinkTarget, updateEntry, re
|
|
|
19
18
|
import { isGeneratedCoreMirrorKey, PERSONAL_VAULT_MANIFEST_KEY, } from "../personal-vault.js";
|
|
20
19
|
import { isPersonalVaultExcluded } from "../personal-vault-exclusions.js";
|
|
21
20
|
import { buildScopeShrinkPlan, applyScopeShrink, ScopeShrinkBlockedError, ScopeShrinkLargePruneError, } from "../scope-shrink.js";
|
|
22
|
-
import { coalescePrefixes,
|
|
23
|
-
import { listRemoteForScope, POST_FILTER_THRESHOLD } from "../remote-pull.js";
|
|
21
|
+
import { coalescePrefixes, isCoveredByAny, } from "../prefix-coalesce.js";
|
|
24
22
|
import { createIgnoreFilter } from "../ignore.js";
|
|
25
23
|
import { hasRemoteChanged, isAccessDenied, resolveActiveCompany, resolveTransferConcurrency, readHostLoad, } from "../sync-core.js";
|
|
26
24
|
import { isEphemeralPath, isForbiddenCompanyVaultKey, isMalformedVaultKey } from "./share.js";
|
|
@@ -328,19 +326,7 @@ async function buildPullContext(options) {
|
|
|
328
326
|
// of the scope-shrink inputs (see `planScopeShrink`). Empty when the caller
|
|
329
327
|
// passes nothing (legacy behavior: no exclusions).
|
|
330
328
|
const currentExcludeSet = coalescePrefixes(options.excludePrefixes ?? []);
|
|
331
|
-
|
|
332
|
-
// with the durable scope so the LISTED scope can only shrink, never widen.
|
|
333
|
-
// A path may be a file or a directory — we can't know without I/O — so each
|
|
334
|
-
// requested path contributes both its exact and its directory spelling,
|
|
335
|
-
// mirroring `effectivePushPrefixSet` on the push side.
|
|
336
|
-
const requestedPullPrefixes = coalescePrefixes((options.pullPrefixes ?? []).flatMap((p) => [
|
|
337
|
-
pathToScopePrefix(p),
|
|
338
|
-
pathToScopePrefix(p.endsWith("/") ? p : p + "/"),
|
|
339
|
-
]));
|
|
340
|
-
const listedPrefixSet = requestedPullPrefixes.length === 0
|
|
341
|
-
? currentPrefixSet
|
|
342
|
-
: intersectPrefixSets(currentPrefixSet, requestedPullPrefixes);
|
|
343
|
-
const remoteFiles = await listRemoteFilesForScope(ctx, syncMode, listedPrefixSet);
|
|
329
|
+
const remoteFiles = await listRemoteFiles(ctx);
|
|
344
330
|
const fileTombstones = options.personalMode === true
|
|
345
331
|
? new Map()
|
|
346
332
|
: await fetchCompanyTombstones(vaultConfig, ctx.uid);
|
|
@@ -360,60 +346,13 @@ async function buildPullContext(options) {
|
|
|
360
346
|
remoteFiles,
|
|
361
347
|
syncMode,
|
|
362
348
|
currentPrefixSet,
|
|
363
|
-
listedPrefixSet,
|
|
364
349
|
currentExcludeSet,
|
|
365
350
|
fileTombstones,
|
|
366
351
|
downloadsSinceJournalCheckpoint: 0,
|
|
367
352
|
};
|
|
368
353
|
}
|
|
369
|
-
/**
|
|
370
|
-
* List the remote objects the current pull can actually materialize.
|
|
371
|
-
*
|
|
372
|
-
* Historically this was ALWAYS a full-bucket `listRemoteFiles(ctx)` — even for
|
|
373
|
-
* a `shared`/`custom` membership whose effective scope is a handful of
|
|
374
|
-
* prefixes — so every pull materialized the entire remote listing (tens of
|
|
375
|
-
* thousands of RemoteFile objects on large vaults) only for `computePullPlan`
|
|
376
|
-
* to discard most of it as `skip-out-of-scope`. Scoped pulls now list ONLY the
|
|
377
|
-
* in-scope prefixes via `listRemoteForScope` (per-prefix ListObjectsV2 calls,
|
|
378
|
-
* bounded parallel, deduped), which bounds the listing's memory to the scope's
|
|
379
|
-
* actual footprint.
|
|
380
|
-
*
|
|
381
|
-
* `all`-mode (an unnarrowed full-vault pull, `listedPrefixSet` `[""]`) keeps
|
|
382
|
-
* the single broad listing: fanning `all` out by top-level prefix would need a
|
|
383
|
-
* delimiter LIST to discover the top-level names first and still union the
|
|
384
|
-
* same total object count in one plan, so it bounds nothing today. (A
|
|
385
|
-
* streaming planner is the real fix for `all` and is out of scope here.)
|
|
386
|
-
*
|
|
387
|
-
* An empty `listedPrefixSet` in a scoped mode means "nothing in scope" —
|
|
388
|
-
* issue zero LIST calls. Consumers already treat an empty listing correctly:
|
|
389
|
-
* the deletion sweep in `computePullPlan` only considers journal keys covered
|
|
390
|
-
* by the LISTED scope, so keys absent because they were never listed can never
|
|
391
|
-
* be mistaken for remote deletes.
|
|
392
|
-
*/
|
|
393
|
-
async function listRemoteFilesForScope(ctx, syncMode, listedPrefixSet) {
|
|
394
|
-
const entries = toScopePrefixEntries(listedPrefixSet);
|
|
395
|
-
if (entries.some((e) => e.prefix === ""))
|
|
396
|
-
return listRemoteFiles(ctx);
|
|
397
|
-
if (entries.length === 0) {
|
|
398
|
-
// `all` mode always carries `[""]`, so an empty set only occurs for a
|
|
399
|
-
// scoped mode (empty grants, or a targeted intersection that excluded
|
|
400
|
-
// everything). Defensive `all` branch: never silently narrow a full pull.
|
|
401
|
-
return syncMode === "all" ? listRemoteFiles(ctx) : [];
|
|
402
|
-
}
|
|
403
|
-
return listRemoteForScope({
|
|
404
|
-
ctx,
|
|
405
|
-
scope: {
|
|
406
|
-
companyUid: ctx.uid,
|
|
407
|
-
syncMode,
|
|
408
|
-
prefixSet: listedPrefixSet,
|
|
409
|
-
strategy: listedPrefixSet.length > POST_FILTER_THRESHOLD
|
|
410
|
-
? "broad-postfilter"
|
|
411
|
-
: "vend-fanout",
|
|
412
|
-
},
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
354
|
function planPull(run) {
|
|
416
|
-
return computePullPlan(run.remoteFiles, run.journal, run.companyRoot, run.shouldSync, run.options.personalMode === true, run.options.teamSyncedSlugs ?? null, run.
|
|
355
|
+
return computePullPlan(run.remoteFiles, run.journal, run.companyRoot, run.shouldSync, run.options.personalMode === true, run.options.teamSyncedSlugs ?? null, run.currentPrefixSet, run.fileTombstones, run.currentExcludeSet, run.options.onHeartbeat);
|
|
417
356
|
}
|
|
418
357
|
function emitPullPlan(emit, plan) {
|
|
419
358
|
emit({
|
|
@@ -687,13 +626,12 @@ async function executeConflictItem(run, plan, scopeRun, counters, downloadItems,
|
|
|
687
626
|
// the `.conflict-` mirror loop AND the journal false-stamp (remote etag over
|
|
688
627
|
// divergent local content) that the keep path produced for these files.
|
|
689
628
|
if (isCloudAuthoritative(vaultKeyForLocalPath(run.hqRoot, localPath))) {
|
|
690
|
-
const currentSnapshot = snapshotLocalState(localPath);
|
|
691
629
|
downloadItems.push({
|
|
692
630
|
action: "download",
|
|
693
631
|
remoteFile,
|
|
694
632
|
localPath,
|
|
695
633
|
isNew: false,
|
|
696
|
-
localSnapshot:
|
|
634
|
+
localSnapshot: item.localSnapshot,
|
|
697
635
|
});
|
|
698
636
|
run.emit({ type: "reconciled", path: remoteFile.key, direction: "pull" });
|
|
699
637
|
return null;
|
|
@@ -716,24 +654,9 @@ async function executeConflictItem(run, plan, scopeRun, counters, downloadItems,
|
|
|
716
654
|
}
|
|
717
655
|
let remoteFetched = false;
|
|
718
656
|
let converged = false;
|
|
719
|
-
let executionLocal = null;
|
|
720
657
|
try {
|
|
721
658
|
const downloaded = await downloadFile(run.ctx, remoteFile.key, conflictAbs);
|
|
722
659
|
remoteFetched = true;
|
|
723
|
-
executionLocal = readCurrentLocalFileState(localPath);
|
|
724
|
-
if (executionLocal === null) {
|
|
725
|
-
// The path changed kind, disappeared, or kept moving while the remote
|
|
726
|
-
// probe was in flight. Leave both sides untouched and let the next
|
|
727
|
-
// per-file pass classify that new state; a stale conflict is not useful.
|
|
728
|
-
try {
|
|
729
|
-
fs.rmSync(conflictAbs, { force: true });
|
|
730
|
-
}
|
|
731
|
-
catch {
|
|
732
|
-
/* best-effort cleanup */
|
|
733
|
-
}
|
|
734
|
-
counters.filesSkipped++;
|
|
735
|
-
return null;
|
|
736
|
-
}
|
|
737
660
|
if (fs.lstatSync(conflictAbs).isSymbolicLink()) {
|
|
738
661
|
const target = readlinkOrNull(conflictAbs);
|
|
739
662
|
if (target === null) {
|
|
@@ -747,12 +670,11 @@ async function executeConflictItem(run, plan, scopeRun, counters, downloadItems,
|
|
|
747
670
|
return null;
|
|
748
671
|
}
|
|
749
672
|
else {
|
|
750
|
-
converged = hashSymlinkTarget(target) ===
|
|
673
|
+
converged = hashSymlinkTarget(target) === item.localHash;
|
|
751
674
|
}
|
|
752
675
|
}
|
|
753
676
|
else {
|
|
754
|
-
converged =
|
|
755
|
-
(downloaded.contentHash ?? hashFile(conflictAbs)) === executionLocal.hash;
|
|
677
|
+
converged = (downloaded.contentHash ?? hashFile(conflictAbs)) === item.localHash;
|
|
756
678
|
}
|
|
757
679
|
}
|
|
758
680
|
catch (probeErr) {
|
|
@@ -780,10 +702,6 @@ async function executeConflictItem(run, plan, scopeRun, counters, downloadItems,
|
|
|
780
702
|
});
|
|
781
703
|
}
|
|
782
704
|
if (converged) {
|
|
783
|
-
if (executionLocal === null) {
|
|
784
|
-
counters.filesSkipped++;
|
|
785
|
-
return null;
|
|
786
|
-
}
|
|
787
705
|
if (remoteFetched) {
|
|
788
706
|
try {
|
|
789
707
|
fs.rmSync(conflictAbs, { force: true });
|
|
@@ -792,11 +710,10 @@ async function executeConflictItem(run, plan, scopeRun, counters, downloadItems,
|
|
|
792
710
|
/* best-effort cleanup; a stray identical mirror is harmless */
|
|
793
711
|
}
|
|
794
712
|
}
|
|
795
|
-
updateEntry(run.journal, remoteFile.key,
|
|
713
|
+
updateEntry(run.journal, remoteFile.key, item.localHash, item.localSize, "down", localPath, {
|
|
796
714
|
remoteEtag: remoteFile.etag,
|
|
797
|
-
mtimeMs:
|
|
798
|
-
|
|
799
|
-
kind: executionLocal.kind,
|
|
715
|
+
mtimeMs: item.localMtime.getTime(),
|
|
716
|
+
kind: item.localSnapshot.kind === "symlink" ? "symlink" : "file",
|
|
800
717
|
});
|
|
801
718
|
run.emit({ type: "reconciled", path: remoteFile.key, direction: "pull" });
|
|
802
719
|
counters.filesSkipped++;
|
|
@@ -804,20 +721,11 @@ async function executeConflictItem(run, plan, scopeRun, counters, downloadItems,
|
|
|
804
721
|
}
|
|
805
722
|
counters.conflicts++;
|
|
806
723
|
counters.conflictPaths.push(remoteFile.key);
|
|
807
|
-
// A successful probe always decides from the local bytes captured after the
|
|
808
|
-
// GET. Only a failed probe falls back to the preview snapshot so the existing
|
|
809
|
-
// error-path behavior remains conservative.
|
|
810
|
-
const localHash = executionLocal?.hash ?? item.localHash;
|
|
811
|
-
const localSize = executionLocal?.size ?? item.localSize;
|
|
812
|
-
const localMtime = executionLocal?.mtime ?? item.localMtime;
|
|
813
|
-
const localSnapshot = executionLocal?.snapshot ?? item.localSnapshot;
|
|
814
|
-
const localKind = executionLocal?.kind ??
|
|
815
|
-
(item.localSnapshot.kind === "symlink" ? "symlink" : "file");
|
|
816
724
|
const resolution = await resolveConflict({
|
|
817
725
|
path: remoteFile.key,
|
|
818
|
-
localHash,
|
|
726
|
+
localHash: item.localHash,
|
|
819
727
|
remoteModified: remoteFile.lastModified,
|
|
820
|
-
localModified: localMtime,
|
|
728
|
+
localModified: item.localMtime,
|
|
821
729
|
direction: "pull",
|
|
822
730
|
}, run.options.onConflict);
|
|
823
731
|
run.emit({
|
|
@@ -826,9 +734,7 @@ async function executeConflictItem(run, plan, scopeRun, counters, downloadItems,
|
|
|
826
734
|
direction: "pull",
|
|
827
735
|
resolution,
|
|
828
736
|
});
|
|
829
|
-
if (resolution !== "abort" &&
|
|
830
|
-
resolution !== "overwrite" &&
|
|
831
|
-
resolution !== "keep") {
|
|
737
|
+
if (resolution !== "abort" && resolution !== "overwrite") {
|
|
832
738
|
if (remoteFetched) {
|
|
833
739
|
try {
|
|
834
740
|
appendConflictEntry(run.hqRoot, {
|
|
@@ -838,12 +744,8 @@ async function executeConflictItem(run, plan, scopeRun, counters, downloadItems,
|
|
|
838
744
|
detectedAt,
|
|
839
745
|
side: "pull",
|
|
840
746
|
machineId,
|
|
841
|
-
localHash,
|
|
747
|
+
localHash: item.localHash,
|
|
842
748
|
remoteHash: remoteFile.etag ? normalizeEtag(remoteFile.etag) : "",
|
|
843
|
-
// The holding resolutions leave local alone and this sidecar keeps
|
|
844
|
-
// the cloud copy. `keep` is indexed only after its local rename
|
|
845
|
-
// succeeds below, so this row can never describe the wrong side.
|
|
846
|
-
preserved: "remote",
|
|
847
749
|
});
|
|
848
750
|
}
|
|
849
751
|
catch (mirrorErr) {
|
|
@@ -855,7 +757,7 @@ async function executeConflictItem(run, plan, scopeRun, counters, downloadItems,
|
|
|
855
757
|
}
|
|
856
758
|
}
|
|
857
759
|
}
|
|
858
|
-
else if (
|
|
760
|
+
else if (remoteFetched) {
|
|
859
761
|
try {
|
|
860
762
|
fs.rmSync(conflictAbs, { force: true });
|
|
861
763
|
}
|
|
@@ -883,103 +785,22 @@ async function executeConflictItem(run, plan, scopeRun, counters, downloadItems,
|
|
|
883
785
|
changedPaths: [...counters.changedPaths, ...scopeRun.changedPaths],
|
|
884
786
|
};
|
|
885
787
|
}
|
|
886
|
-
|
|
887
|
-
// cannot re-fire (#137) and arming the journal-honesty flag because the held
|
|
888
|
-
// copy never matched that etag. The flag stops the currency-gated delete
|
|
889
|
-
// planner propagating a delete whose currency would falsely match on HEAD.
|
|
890
|
-
// Any genuine future download clears it by replacing the entry.
|
|
891
|
-
const holdLocal = () => {
|
|
788
|
+
if (resolution === "keep" || resolution === "skip") {
|
|
892
789
|
counters.filesSkipped++;
|
|
893
|
-
updateEntry(run.journal, remoteFile.key, localHash, localSize, "down", localPath, {
|
|
790
|
+
updateEntry(run.journal, remoteFile.key, item.localHash, item.localSize, "down", localPath, {
|
|
894
791
|
remoteEtag: remoteFile.etag,
|
|
895
|
-
mtimeMs: localMtime.getTime(),
|
|
896
|
-
kind:
|
|
897
|
-
});
|
|
898
|
-
const heldEntry = getEntry(run.journal, remoteFile.key);
|
|
899
|
-
if (heldEntry)
|
|
900
|
-
heldEntry.localDiverges = true;
|
|
901
|
-
return null;
|
|
902
|
-
};
|
|
903
|
-
// `publish-local` is a PUSH-leg strategy: it means "send my body up". On the
|
|
904
|
-
// pull leg there is nothing to publish, so it holds the local body and arms
|
|
905
|
-
// the honesty flag, which is what the push leg then promotes under an
|
|
906
|
-
// If-Match fence. Falling through to the download branch instead would
|
|
907
|
-
// overwrite the very local copy the operator elected, which is the data loss
|
|
908
|
-
// this strategy exists to prevent.
|
|
909
|
-
//
|
|
910
|
-
// `skip` is the interactive "leave this one, I will deal with it later". A
|
|
911
|
-
// resolution that says leave it alone may not rename the operator's file, so
|
|
912
|
-
// it holds too — it is deliberately NOT bundled with `keep` any more.
|
|
913
|
-
if (resolution === "skip" || resolution === "publish-local") {
|
|
914
|
-
return holdLocal();
|
|
915
|
-
}
|
|
916
|
-
// `keep` ADOPTS the cloud body as the working copy and preserves the local
|
|
917
|
-
// one beside it under the `.conflict-` name.
|
|
918
|
-
//
|
|
919
|
-
// It used to mean the opposite — hold local, stamp the remote etag, arm
|
|
920
|
-
// `localDiverges` — and that combination was terminal. The push leg refuses
|
|
921
|
-
// to send a body the journal calls divergent; the pull leg sees a remote
|
|
922
|
-
// whose etag already matches and leaves it alone; and every later `keep`
|
|
923
|
-
// re-armed the flag. The file converged in neither direction, and because the
|
|
924
|
-
// desktop app hardcodes `--on-conflict keep` that was the default outcome for
|
|
925
|
-
// every real conflict. Adopting the remote clears the divergence by
|
|
926
|
-
// construction: the entry then describes the body that is actually on disk.
|
|
927
|
-
// The local bytes are not destroyed, only renamed.
|
|
928
|
-
if (resolution === "keep") {
|
|
929
|
-
if (!remoteFetched) {
|
|
930
|
-
// The probe never produced a body (its failure is already emitted above).
|
|
931
|
-
// Hold rather than move aside a file we have nothing to replace it with.
|
|
932
|
-
return holdLocal();
|
|
933
|
-
}
|
|
934
|
-
try {
|
|
935
|
-
// The sidecar currently holds the fetched probe copy of the remote.
|
|
936
|
-
// Remove it explicitly before renaming: POSIX rename replaces a file at
|
|
937
|
-
// the destination, but Windows rename rejects an occupied destination.
|
|
938
|
-
// The download queued below re-fetches it. One extra GET buys the entire
|
|
939
|
-
// tested download path —
|
|
940
|
-
// symlink materialization, path codec, mtime and etag journalling —
|
|
941
|
-
// instead of a hand-rolled three-way swap with its own crash window.
|
|
942
|
-
fs.rmSync(conflictAbs, { force: true });
|
|
943
|
-
fs.renameSync(localPath, conflictAbs);
|
|
944
|
-
}
|
|
945
|
-
catch (preserveErr) {
|
|
946
|
-
run.emit({
|
|
947
|
-
type: "error",
|
|
948
|
-
path: remoteFile.key,
|
|
949
|
-
message: `conflict keep could not preserve the local copy: ${preserveErr instanceof Error ? preserveErr.message : String(preserveErr)}`,
|
|
950
|
-
});
|
|
951
|
-
return holdLocal();
|
|
952
|
-
}
|
|
953
|
-
try {
|
|
954
|
-
appendConflictEntry(run.hqRoot, {
|
|
955
|
-
id: buildConflictId(originalRelative, detectedAt),
|
|
956
|
-
originalPath: originalRelative,
|
|
957
|
-
conflictPath: conflictRelative,
|
|
958
|
-
detectedAt,
|
|
959
|
-
side: "pull",
|
|
960
|
-
machineId,
|
|
961
|
-
localHash,
|
|
962
|
-
remoteHash: remoteFile.etag ? normalizeEtag(remoteFile.etag) : "",
|
|
963
|
-
preserved: "local",
|
|
964
|
-
});
|
|
965
|
-
}
|
|
966
|
-
catch (mirrorErr) {
|
|
967
|
-
run.emit({
|
|
968
|
-
type: "error",
|
|
969
|
-
path: remoteFile.key,
|
|
970
|
-
message: `conflict mirror index write failed: ${mirrorErr instanceof Error ? mirrorErr.message : String(mirrorErr)}`,
|
|
971
|
-
});
|
|
972
|
-
}
|
|
973
|
-
downloadItems.push({
|
|
974
|
-
action: "download",
|
|
975
|
-
remoteFile,
|
|
976
|
-
localPath,
|
|
977
|
-
isNew: false,
|
|
978
|
-
// The local body was just moved aside, so the pre-replace guard must
|
|
979
|
-
// expect absence. Passing the original snapshot would make that guard
|
|
980
|
-
// abort the very download it exists to protect.
|
|
981
|
-
localSnapshot: { kind: "absent" },
|
|
792
|
+
mtimeMs: item.localMtime.getTime(),
|
|
793
|
+
kind: item.localSnapshot.kind === "symlink" ? "symlink" : "file",
|
|
982
794
|
});
|
|
795
|
+
// Journal-honesty: we recorded the remote etag so this conflict can't
|
|
796
|
+
// re-fire (#137), but the KEPT local copy diverges from that remote — it
|
|
797
|
+
// never matched it. Flag the entry so the currency-gated delete planner
|
|
798
|
+
// refuses to propagate a delete for it (its currency would falsely match on
|
|
799
|
+
// HEAD, and the delete would destroy the divergent remote version). Any
|
|
800
|
+
// genuine future download clears the flag by replacing the entry.
|
|
801
|
+
const keptEntry = getEntry(run.journal, remoteFile.key);
|
|
802
|
+
if (keptEntry)
|
|
803
|
+
keptEntry.localDiverges = true;
|
|
983
804
|
return null;
|
|
984
805
|
}
|
|
985
806
|
downloadItems.push({
|
|
@@ -987,7 +808,7 @@ async function executeConflictItem(run, plan, scopeRun, counters, downloadItems,
|
|
|
987
808
|
remoteFile,
|
|
988
809
|
localPath,
|
|
989
810
|
isNew: false,
|
|
990
|
-
localSnapshot,
|
|
811
|
+
localSnapshot: item.localSnapshot,
|
|
991
812
|
});
|
|
992
813
|
return null;
|
|
993
814
|
}
|
|
@@ -1070,12 +891,6 @@ async function downloadOne(run, downloadItem, counters) {
|
|
|
1070
891
|
updateEntry(run.journal, remoteFile.key, hash, size, "down", localPath, {
|
|
1071
892
|
remoteEtag: remoteFile.etag,
|
|
1072
893
|
mtimeMs: localLstat.mtimeMs,
|
|
1073
|
-
// Stamped from the lstat already in hand so the next push can skip this
|
|
1074
|
-
// file on stat alone. The two conflict-resolution `updateEntry` calls
|
|
1075
|
-
// above deliberately do NOT stamp ctime: neither has a post-write stat
|
|
1076
|
-
// available, and a `keep`/`publish-local` entry is marked
|
|
1077
|
-
// `localDiverges` immediately after, which the fast path refuses anyway.
|
|
1078
|
-
ctimeMs: localLstat.ctimeMs,
|
|
1079
894
|
...(createdBySub !== undefined ? { createdBySub } : {}),
|
|
1080
895
|
kind: isLocalSymlink ? "symlink" : "file",
|
|
1081
896
|
});
|
|
@@ -1137,7 +952,7 @@ async function downloadOne(run, downloadItem, counters) {
|
|
|
1137
952
|
run.emit({
|
|
1138
953
|
type: "error",
|
|
1139
954
|
path: remoteFile.key,
|
|
1140
|
-
message:
|
|
955
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1141
956
|
});
|
|
1142
957
|
}
|
|
1143
958
|
return;
|
|
@@ -1370,12 +1185,6 @@ function finalizePullRun(run, plan, scopeRun, counters) {
|
|
|
1370
1185
|
* -files loop this event exists to end. `SyncResult.filesOutOfScope` keeps its
|
|
1371
1186
|
* original both-causes meaning; only this advisory is narrowed.
|
|
1372
1187
|
*/
|
|
1373
|
-
// NOTE: since scoped pulls list only their in-scope prefixes (see
|
|
1374
|
-
// `listRemoteFilesForScope`), out-of-scope remote keys usually never enter the
|
|
1375
|
-
// plan and this event fires only for keys a broad-postfilter listing (or a
|
|
1376
|
-
// legacy caller passing a full listing) still surfaced. The withheld-count
|
|
1377
|
-
// telemetry traded away here is the cost of not materializing the full remote
|
|
1378
|
-
// listing on every scoped pull.
|
|
1379
1188
|
function emitScopeMaterializationGap(run, plan) {
|
|
1380
1189
|
if (run.syncMode === "all")
|
|
1381
1190
|
return;
|
|
@@ -1505,36 +1314,6 @@ function matchesLocalSnapshot(localPath, expected) {
|
|
|
1505
1314
|
return ((actual.kind !== "file" && actual.kind !== "symlink") ||
|
|
1506
1315
|
actual.hash === expected.hash);
|
|
1507
1316
|
}
|
|
1508
|
-
/**
|
|
1509
|
-
* Read the local side at the execution seam, not from the whole-run preview.
|
|
1510
|
-
* A second hash check closes the read-to-stat window: when an active writer
|
|
1511
|
-
* moves the file while we inspect it, the caller defers this path instead of
|
|
1512
|
-
* manufacturing a decision from a mixed snapshot.
|
|
1513
|
-
*/
|
|
1514
|
-
function readCurrentLocalFileState(localPath) {
|
|
1515
|
-
const snapshot = snapshotLocalState(localPath);
|
|
1516
|
-
if (snapshot === null || (snapshot.kind !== "file" && snapshot.kind !== "symlink")) {
|
|
1517
|
-
return null;
|
|
1518
|
-
}
|
|
1519
|
-
let lstat;
|
|
1520
|
-
try {
|
|
1521
|
-
lstat = fs.lstatSync(localPath);
|
|
1522
|
-
}
|
|
1523
|
-
catch {
|
|
1524
|
-
return null;
|
|
1525
|
-
}
|
|
1526
|
-
if (!matchesLocalSnapshot(localPath, snapshot))
|
|
1527
|
-
return null;
|
|
1528
|
-
return {
|
|
1529
|
-
hash: snapshot.hash,
|
|
1530
|
-
size: snapshot.kind === "symlink" ? 0 : lstat.size,
|
|
1531
|
-
mtime: lstat.mtime,
|
|
1532
|
-
mtimeMs: lstat.mtimeMs,
|
|
1533
|
-
ctimeMs: lstat.ctimeMs,
|
|
1534
|
-
kind: snapshot.kind,
|
|
1535
|
-
snapshot,
|
|
1536
|
-
};
|
|
1537
|
-
}
|
|
1538
1317
|
function hasCurrentLocalDeleteIntent(entry) {
|
|
1539
1318
|
const intent = entry?.localDeleteIntent;
|
|
1540
1319
|
return !!(intent &&
|
|
@@ -2354,17 +2133,6 @@ excludePrefixes = [], onHeartbeat) {
|
|
|
2354
2133
|
const posixKey = toPosixKey(key);
|
|
2355
2134
|
if (remoteKeySet.has(posixKey))
|
|
2356
2135
|
continue;
|
|
2357
|
-
// Listing-scope guard (targeted sync / scoped listing). The remote LIST
|
|
2358
|
-
// now covers ONLY `prefixSet` (scoped modes list per-prefix; a targeted
|
|
2359
|
-
// pull narrows further), so "absent from the LIST" is evidence of a remote
|
|
2360
|
-
// delete ONLY for keys the LIST could have contained. A journal key
|
|
2361
|
-
// outside the listed scope was simply never asked about — treating it as
|
|
2362
|
-
// remote-deleted would mass-delete every out-of-scope local file on the
|
|
2363
|
-
// first scoped pull. Out-of-scope journal entries stay the scope-shrink
|
|
2364
|
-
// pass's responsibility (quarantine semantics), never this sweep's.
|
|
2365
|
-
// `all` mode passes `[""]`, which covers every key — no behavior change.
|
|
2366
|
-
if (!isCoveredByAny(posixKey, prefixSet))
|
|
2367
|
-
continue;
|
|
2368
2136
|
if (posixKey.startsWith(SKILLS_KEY_PREFIX) &&
|
|
2369
2137
|
remoteCanonicalSkillKeys.has(canonicalVaultKeySpelling(posixKey)) &&
|
|
2370
2138
|
// An EXACT FILE_TOMBSTONE for the journaled key is an authoritative
|