@indigoai-us/hq-cloud 6.18.53 → 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.
Files changed (87) hide show
  1. package/dist/bin/sync-runner-audit.d.ts +26 -0
  2. package/dist/bin/sync-runner-audit.d.ts.map +1 -0
  3. package/dist/bin/sync-runner-audit.js +51 -0
  4. package/dist/bin/sync-runner-audit.js.map +1 -0
  5. package/dist/bin/sync-runner-company.d.ts +18 -0
  6. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  7. package/dist/bin/sync-runner-company.js +186 -9
  8. package/dist/bin/sync-runner-company.js.map +1 -1
  9. package/dist/bin/sync-runner-watch-loop.d.ts +107 -5
  10. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  11. package/dist/bin/sync-runner-watch-loop.js +1382 -183
  12. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  13. package/dist/bin/sync-runner.d.ts +63 -11
  14. package/dist/bin/sync-runner.d.ts.map +1 -1
  15. package/dist/bin/sync-runner.js +348 -94
  16. package/dist/bin/sync-runner.js.map +1 -1
  17. package/dist/bin/watcher-catchup-skip-store.d.ts +60 -0
  18. package/dist/bin/watcher-catchup-skip-store.d.ts.map +1 -0
  19. package/dist/bin/watcher-catchup-skip-store.js +282 -0
  20. package/dist/bin/watcher-catchup-skip-store.js.map +1 -0
  21. package/dist/cli/conflict.d.ts +6 -0
  22. package/dist/cli/conflict.d.ts.map +1 -1
  23. package/dist/cli/conflict.js +3 -1
  24. package/dist/cli/conflict.js.map +1 -1
  25. package/dist/cli/share.d.ts +27 -0
  26. package/dist/cli/share.d.ts.map +1 -1
  27. package/dist/cli/share.js +108 -15
  28. package/dist/cli/share.js.map +1 -1
  29. package/dist/cli/sync.d.ts +18 -2
  30. package/dist/cli/sync.d.ts.map +1 -1
  31. package/dist/cli/sync.js +74 -14
  32. package/dist/cli/sync.js.map +1 -1
  33. package/dist/ignore.d.ts +1 -1
  34. package/dist/ignore.d.ts.map +1 -1
  35. package/dist/ignore.js +2 -2
  36. package/dist/ignore.js.map +1 -1
  37. package/dist/journal-snapshot-v4.d.ts.map +1 -1
  38. package/dist/journal-snapshot-v4.js +44 -38
  39. package/dist/journal-snapshot-v4.js.map +1 -1
  40. package/dist/journal.d.ts +15 -2
  41. package/dist/journal.d.ts.map +1 -1
  42. package/dist/journal.js +221 -16
  43. package/dist/journal.js.map +1 -1
  44. package/dist/lib/cloud-authoritative.d.ts +9 -2
  45. package/dist/lib/cloud-authoritative.d.ts.map +1 -1
  46. package/dist/lib/cloud-authoritative.js +34 -2
  47. package/dist/lib/cloud-authoritative.js.map +1 -1
  48. package/dist/lib/exit-codes.d.ts +2 -0
  49. package/dist/lib/exit-codes.d.ts.map +1 -1
  50. package/dist/lib/exit-codes.js +2 -0
  51. package/dist/lib/exit-codes.js.map +1 -1
  52. package/dist/personal-vault-exclusions.d.ts +2 -2
  53. package/dist/personal-vault-exclusions.d.ts.map +1 -1
  54. package/dist/personal-vault-exclusions.js +5 -3
  55. package/dist/personal-vault-exclusions.js.map +1 -1
  56. package/dist/personal-vault.d.ts +7 -0
  57. package/dist/personal-vault.d.ts.map +1 -1
  58. package/dist/personal-vault.js +10 -0
  59. package/dist/personal-vault.js.map +1 -1
  60. package/dist/qmd-runner-admission.d.ts +22 -0
  61. package/dist/qmd-runner-admission.d.ts.map +1 -0
  62. package/dist/qmd-runner-admission.js +107 -0
  63. package/dist/qmd-runner-admission.js.map +1 -0
  64. package/dist/symlink-target-policy.d.ts +11 -0
  65. package/dist/symlink-target-policy.d.ts.map +1 -1
  66. package/dist/symlink-target-policy.js +110 -0
  67. package/dist/symlink-target-policy.js.map +1 -1
  68. package/dist/sync/mutation-session.d.ts.map +1 -1
  69. package/dist/sync/mutation-session.js +4 -0
  70. package/dist/sync/mutation-session.js.map +1 -1
  71. package/dist/sync/parallel-sync-coordinator.d.ts +29 -0
  72. package/dist/sync/parallel-sync-coordinator.d.ts.map +1 -1
  73. package/dist/sync/parallel-sync-coordinator.js +229 -16
  74. package/dist/sync/parallel-sync-coordinator.js.map +1 -1
  75. package/dist/sync/push-receiver.d.ts +5 -0
  76. package/dist/sync/push-receiver.d.ts.map +1 -1
  77. package/dist/sync/push-receiver.js +32 -10
  78. package/dist/sync/push-receiver.js.map +1 -1
  79. package/dist/watch-owner-lease.d.ts +4 -0
  80. package/dist/watch-owner-lease.d.ts.map +1 -1
  81. package/dist/watch-owner-lease.js +40 -6
  82. package/dist/watch-owner-lease.js.map +1 -1
  83. package/dist/watcher.d.ts +16 -3
  84. package/dist/watcher.d.ts.map +1 -1
  85. package/dist/watcher.js +114 -21
  86. package/dist/watcher.js.map +1 -1
  87. 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 { isCloudAuthoritative } from "../lib/cloud-authoritative.js";
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 isCloudAuthoritative(relativePath);
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 collected = await collectFiles(run.paths, run.hqRoot, run.syncRoot, run.shouldSync, {
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
@@ -2405,8 +2442,38 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2405
2442
  }
2406
2443
  planLimitPauses.save(now);
2407
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
+ };
2408
2473
  const settleFreshSkip = (item) => {
2409
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.
2410
2477
  if (run.options.serverOwnedPathPolicy !== "skip") {
2411
2478
  emitServerOwnedRefusal(run, item.relativePath);
2412
2479
  }
@@ -2500,8 +2567,10 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2500
2567
  // than for the `localDiverges` recovery. The two cases need different write
2501
2568
  // fences — see the precondition block below.
2502
2569
  let fenceOnObservedRemote = false;
2570
+ let remoteChangedSinceJournal = false;
2503
2571
  // Cloud-authoritative paths (server-regenerated: company-brief.md, board.json,
2504
- // ontology/ signals/ sources/) are PULL-WINS and server-owned. The push leg must
2572
+ // pipeline-owned ontology/, signals/, and sources/) are PULL-WINS and server-owned.
2573
+ // The push leg must
2505
2574
  // NEVER upload the local copy over them — not just on a two-sided conflict, but on
2506
2575
  // ANY divergence, including a one-sided local change. A stale local mirror (e.g. a
2507
2576
  // second HQ root that shares this machine's sync journal) would otherwise read as
@@ -2532,6 +2601,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2532
2601
  catch (headErr) {
2533
2602
  if (isAccessDenied(headErr)) {
2534
2603
  run.scopeExcludedSet.add(relativePath);
2604
+ recordCatchupSkip(relativePath, absolutePath, "access-denied");
2535
2605
  return;
2536
2606
  }
2537
2607
  throw headErr;
@@ -2543,6 +2613,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2543
2613
  if (remoteMeta &&
2544
2614
  !journalRows(run.journal).has(relativePath) &&
2545
2615
  isArchivedSessionLog(relativePath, remoteMeta)) {
2616
+ recordCatchupSkip(relativePath, absolutePath, "archived-session-log");
2546
2617
  run.emit({ type: "skip-archived-session-log", path: relativePath });
2547
2618
  counters.filesSkipped++;
2548
2619
  return;
@@ -2638,6 +2709,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2638
2709
  const remoteContentHash = item.kind === "file" ? publishedContentHash(remoteMeta.metadata) : undefined;
2639
2710
  const remoteChanged = !!journalEntry &&
2640
2711
  hasRemoteChanged({ ...remoteMeta, contentHash: remoteContentHash }, journalEntry);
2712
+ remoteChangedSinceJournal = remoteChanged;
2641
2713
  // A matching published digest is a content no-op even when SSE-KMS gave
2642
2714
  // this object a fresh ETag. Reconcile the journal to that version without
2643
2715
  // a needless PUT or a phantom two-sided conflict.
@@ -2681,6 +2753,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2681
2753
  // ordinary journaled edit whose remote is unchanged still uploads.
2682
2754
  const needsRemoteBody = !journalEntry || (localChanged && remoteChanged) || !!journalEntry.localDiverges;
2683
2755
  if (needsRemoteBody && isArchivedSessionLog(relativePath, remoteMeta)) {
2756
+ recordCatchupSkip(relativePath, absolutePath, "archived-session-log");
2684
2757
  run.emit({ type: "skip-archived-session-log", path: relativePath });
2685
2758
  counters.filesSkipped++;
2686
2759
  return;
@@ -2806,7 +2879,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2806
2879
  let winnerProbe;
2807
2880
  let resolution;
2808
2881
  if (versionAware) {
2809
- winnerProbe = await probeRemoteForWinner(run, item, remoteMeta.lastModified);
2882
+ winnerProbe = await probeRemoteForWinner(run, item, remoteMeta.lastModified, remoteChangedSinceJournal);
2810
2883
  resolution = "keep";
2811
2884
  }
2812
2885
  else {
@@ -3073,6 +3146,11 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
3073
3146
  settleLandedReconcile(run, relativePath, outcome);
3074
3147
  checkpointShareJournal(run);
3075
3148
  }
3149
+ run.options.onAuditPathEvent?.({
3150
+ path: relativePath,
3151
+ contentHash: localHash,
3152
+ bytes: size,
3153
+ });
3076
3154
  if (alreadyCurrent) {
3077
3155
  run.emit({ type: "reconciled", path: relativePath, direction: "push" });
3078
3156
  }
@@ -3131,7 +3209,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
3131
3209
  // pass starts over); remote winning adopts it exactly as the
3132
3210
  // pre-flight conflict path would have.
3133
3211
  const latest = await headRemoteFile(run.ctx, relativePath);
3134
- const probe = await probeRemoteForWinner(run, item, latest?.lastModified ?? null);
3212
+ const probe = await probeRemoteForWinner(run, item, latest?.lastModified ?? null, true);
3135
3213
  const decision = probe.decision;
3136
3214
  run.emit({
3137
3215
  type: "conflict",
@@ -3515,7 +3593,7 @@ function isArchivedSessionLog(relativePath, remoteMeta) {
3515
3593
  * either side, probe failure); the caller then falls back to the historical
3516
3594
  * `keep` outcome (adopt the remote). Session-level auth refusals propagate.
3517
3595
  */
3518
- async function probeRemoteForWinner(run, item, remoteMtime) {
3596
+ async function probeRemoteForWinner(run, item, remoteMtime, remoteChangedSinceJournal = false) {
3519
3597
  const probeAbs = buildConflictProbePath(run.hqRoot);
3520
3598
  let parked = false;
3521
3599
  let fetched;
@@ -3569,6 +3647,7 @@ async function probeRemoteForWinner(run, item, remoteMtime) {
3569
3647
  remoteBytes: fs.readFileSync(probeAbs),
3570
3648
  localMtime: localStat.mtime,
3571
3649
  remoteMtime,
3650
+ remoteChangedSinceJournal,
3572
3651
  });
3573
3652
  }
3574
3653
  }
@@ -3875,6 +3954,12 @@ async function executeDeletes(run, deletePlan, decommissionPlan, counters, files
3875
3954
  await deleteRemoteFile(run.ctx, relativePath);
3876
3955
  removeEntry(run.journal, relativePath);
3877
3956
  counters.filesDeleted++;
3957
+ run.options.onAuditPathEvent?.({
3958
+ path: relativePath,
3959
+ ...(entry?.remoteEtag ? { etag: entry.remoteEtag } : {}),
3960
+ bytes: size,
3961
+ deleted: true,
3962
+ });
3878
3963
  run.emit({
3879
3964
  type: "progress",
3880
3965
  path: relativePath,
@@ -3945,9 +4030,9 @@ async function executeDeletes(run, deletePlan, decommissionPlan, counters, files
3945
4030
  }
3946
4031
  for (const relativePath of deletePlan.toTombstone) {
3947
4032
  const localPath = localPathForVaultKey(run.syncRoot, relativePath);
4033
+ const entry = journalRows(run.journal).get(relativePath);
3948
4034
  try {
3949
4035
  const lstat = fs.lstatSync(localPath);
3950
- const entry = journalRows(run.journal).get(relativePath);
3951
4036
  if (lstat.isFile()) {
3952
4037
  const localHash = hashFile(localPath);
3953
4038
  if (entry?.hash && entry.hash !== localHash) {
@@ -3998,6 +4083,12 @@ async function executeDeletes(run, deletePlan, decommissionPlan, counters, files
3998
4083
  }
3999
4084
  removeEntry(run.journal, relativePath);
4000
4085
  counters.filesTombstoned++;
4086
+ run.options.onAuditPathEvent?.({
4087
+ path: relativePath,
4088
+ ...(entry?.remoteEtag ? { etag: entry.remoteEtag } : {}),
4089
+ bytes: 0,
4090
+ deleted: true,
4091
+ });
4001
4092
  run.emit({
4002
4093
  type: "progress",
4003
4094
  path: relativePath,
@@ -5331,7 +5422,7 @@ async function computeDeletePlan(journal, syncRoot, scopeRoots, shouldSync, poli
5331
5422
  // True for a COMPANY-scoped push (personalMode !== true). This distinguishes
5332
5423
  // the canonical knowledge-marker layout from the personal-vault layout; it
5333
5424
  // does not reinterpret ordinary vault-relative path segments.
5334
- companyScoped = true, skipSecretPaths = false) {
5425
+ companyScoped = true, skipSecretPaths = false, excludedPaths = new Set()) {
5335
5426
  const plan = { toDelete: [], toTombstone: [], refusedStale: [] };
5336
5427
  if (scopeRoots.length === 0)
5337
5428
  return plan;
@@ -5373,6 +5464,8 @@ companyScoped = true, skipSecretPaths = false) {
5373
5464
  const scopeRootSet = new Set(scopeRoots);
5374
5465
  const everyRowInScope = scopeRootSet.has("");
5375
5466
  for (const [relativeKey, entry] of journalRows(journal).entries()) {
5467
+ if (excludedPaths.has(relativeKey))
5468
+ continue;
5376
5469
  const inScope = everyRowInScope || isUnderAnyRoot(relativeKey, scopeRootSet);
5377
5470
  if (!inScope)
5378
5471
  continue;