@indigoai-us/hq-cloud 6.18.104 → 6.18.106

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 (48) hide show
  1. package/dist/bin/legacy-sync-wake.d.ts +25 -0
  2. package/dist/bin/legacy-sync-wake.d.ts.map +1 -0
  3. package/dist/bin/legacy-sync-wake.js +159 -0
  4. package/dist/bin/legacy-sync-wake.js.map +1 -0
  5. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  6. package/dist/bin/sync-runner-company.js +6 -8
  7. package/dist/bin/sync-runner-company.js.map +1 -1
  8. package/dist/bin/sync-runner-watch-loop.d.ts +2 -0
  9. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  10. package/dist/bin/sync-runner-watch-loop.js +76 -1
  11. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  12. package/dist/bin/sync-runner.d.ts +3 -0
  13. package/dist/bin/sync-runner.d.ts.map +1 -1
  14. package/dist/bin/sync-runner.js +9 -0
  15. package/dist/bin/sync-runner.js.map +1 -1
  16. package/dist/cli/conflict.d.ts +8 -2
  17. package/dist/cli/conflict.d.ts.map +1 -1
  18. package/dist/cli/conflict.js +15 -10
  19. package/dist/cli/conflict.js.map +1 -1
  20. package/dist/cli/share.d.ts +8 -0
  21. package/dist/cli/share.d.ts.map +1 -1
  22. package/dist/cli/share.js +231 -30
  23. package/dist/cli/share.js.map +1 -1
  24. package/dist/cli/sync.d.ts.map +1 -1
  25. package/dist/cli/sync.js +33 -2
  26. package/dist/cli/sync.js.map +1 -1
  27. package/dist/lib/same-path-upload-lock.d.ts +39 -1
  28. package/dist/lib/same-path-upload-lock.d.ts.map +1 -1
  29. package/dist/lib/same-path-upload-lock.js +177 -1
  30. package/dist/lib/same-path-upload-lock.js.map +1 -1
  31. package/dist/object-io.d.ts +12 -0
  32. package/dist/object-io.d.ts.map +1 -1
  33. package/dist/object-io.js +28 -1
  34. package/dist/object-io.js.map +1 -1
  35. package/dist/outposts/command.d.ts.map +1 -1
  36. package/dist/outposts/command.js +3 -1
  37. package/dist/outposts/command.js.map +1 -1
  38. package/dist/outposts/company.d.ts +10 -1
  39. package/dist/outposts/company.d.ts.map +1 -1
  40. package/dist/outposts/company.js +17 -0
  41. package/dist/outposts/company.js.map +1 -1
  42. package/dist/sync-core.d.ts +13 -4
  43. package/dist/sync-core.d.ts.map +1 -1
  44. package/dist/sync-core.js +22 -4
  45. package/dist/sync-core.js.map +1 -1
  46. package/dist/types.d.ts +6 -4
  47. package/dist/types.d.ts.map +1 -1
  48. package/package.json +1 -1
package/dist/cli/share.js CHANGED
@@ -24,7 +24,7 @@ import { readPendingConflictNotices, recordConflictParked } from "../conflict-no
24
24
  import { localPathForVaultKey, vaultKeyForLocalPath } from "../local-path-codec.js";
25
25
  import { uploadFile, uploadSymlink, toPosixKey, headRemoteFile, readVerifiedRemoteSymlinkTarget, deleteRemoteFile, downloadFile, primeObjectTransport, primeUploads, publishedContentHash, setUploadContentHash, WindowsSymlinkPrivilegeError, WINDOWS_SYMLINK_PRIVILEGE_REMEDY, WindowsRenameBlockedError, WINDOWS_RENAME_BLOCKED_REMEDY, renameOrRaiseWindowsBlocked, } from "../s3.js";
26
26
  import * as crypto from "crypto";
27
- import { bindObjectIOToContext, PresignObjectIO, RateLimitedError, resolveObjectIO, S3SdkObjectIO, } from "../object-io.js";
27
+ import { bindObjectIOToContext, PresignObjectIO, RateLimitedError, resolveObjectIO, S3SdkObjectIO, isPresignDeletedNewer, } from "../object-io.js";
28
28
  import { primePrivateFolderUpload } from "../private-folder-transport.js";
29
29
  import { getJournalPath, isUnderAnyRoot, readJournalForPass, bindPersonalVaultJournalToIdentity, 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, getStateDir, personalVaultHasOtherIdentityState, readPersonalVaultOwnerUid, } from "../journal.js";
30
30
  import { admitPushIdentity, machineIdentitySwitchRecorded, notePushIdentityHeld, noteCompanyRebind, } from "../push-identity-fence.js";
@@ -38,11 +38,11 @@ import { createHumanSkipLineLog, formatReconcileSkipLine, isUpToDateSkipReason,
38
38
  import { fetchFileTombstones, } from "./tombstones.js";
39
39
  import { compileScopeMatcher, } from "../prefix-coalesce.js";
40
40
  import { hasRemoteChanged, isAccessDenied, resolveActiveCompany, resolveTransferConcurrency, resolveUploadByteBudget, readHostLoad, } from "../sync-core.js";
41
- import { buildConflictBackupPath, buildConflictId, buildConflictProbePath, readShortMachineId, } from "../lib/conflict-file.js";
41
+ import { buildConflictBackupPath, CONFLICT_BACKUPS_DIR, buildConflictId, buildConflictProbePath, readShortMachineId, } from "../lib/conflict-file.js";
42
42
  import { appendConflictEntry, findReusableConflictBackup } from "../lib/conflict-index.js";
43
43
  import { appendIfUnchanged, isAppendOnlyPath, mergeAppendOnly } from "../lib/append-only-merge.js";
44
44
  import { isBoardJsonPath, mergeBoardJson, replaceIfUnchanged } from "../lib/board-merge.js";
45
- import { findLandedUpload, followLandedUploads, SamePathUploadHold, samePathUploadKey, SamePathUploadSupersededError, } from "../lib/same-path-upload-lock.js";
45
+ import { findLandedUpload, findUploadIntent, followLandedUploads, recordUploadIntent, SamePathUploadHold, samePathUploadKey, SamePathUploadSupersededError, settleUploadIntent, } from "../lib/same-path-upload-lock.js";
46
46
  import { isCloudAuthoritative, isLocallyIneligibleForPush } from "../lib/cloud-authoritative.js";
47
47
  import { groupServerOwnedSkips } from "../lib/server-owned-skips.js";
48
48
  import { isOntologyEntityKey, ONTOLOGY_MULTI_WRITER_FLAG, resolveOntologyEntityPush, } from "../lib/ontology-entity-push.js";
@@ -114,6 +114,60 @@ async function remoteContentDiffers(ctx, relativePath, localHash, hqRoot, symlin
114
114
  }
115
115
  }
116
116
  }
117
+ /**
118
+ * A push refused as `deleted-newer`: keep the live local file, copy its bytes
119
+ * once into `.hq/conflict-backups/` (skipped when a backup with the same bytes
120
+ * already exists for this key, so repeated passes do not pile up copies), and
121
+ * drop the journal row so the pull tombstone sweep cannot delete the file.
122
+ */
123
+ function keepLocalOverDeletedNewer(run, absolutePath, relativePath) {
124
+ let backedUp = false;
125
+ try {
126
+ const localHash = hashFile(absolutePath);
127
+ const vaultKey = vaultKeyForLocalPath(run.hqRoot, absolutePath);
128
+ const probe = localPathForVaultKey(run.hqRoot, buildConflictBackupPath(vaultKey, new Date().toISOString(), readShortMachineId(run.hqRoot)));
129
+ // The backup must stay inside the sync-ignored backups directory. A key
130
+ // that resolves outside the HQ root skips the backup; the live file and
131
+ // the journal drop below still protect the bytes.
132
+ const backupsRoot = path.resolve(run.hqRoot, CONFLICT_BACKUPS_DIR);
133
+ const relToBackups = path.relative(backupsRoot, path.resolve(probe));
134
+ if (relToBackups.startsWith("..") || path.isAbsolute(relToBackups)) {
135
+ throw new Error("backup path escapes the conflict-backups directory");
136
+ }
137
+ const dir = path.dirname(probe);
138
+ const base = path.basename(vaultKey);
139
+ const existing = fs.existsSync(dir)
140
+ ? fs.readdirSync(dir).filter((name) => name.startsWith(`${base}.`) && name !== base)
141
+ : [];
142
+ const already = existing.some((name) => {
143
+ try {
144
+ return hashFile(path.join(dir, name)) === localHash;
145
+ }
146
+ catch {
147
+ return false;
148
+ }
149
+ });
150
+ if (!already) {
151
+ fs.mkdirSync(dir, { recursive: true });
152
+ fs.copyFileSync(absolutePath, probe, fs.constants.COPYFILE_EXCL);
153
+ }
154
+ backedUp = true;
155
+ }
156
+ catch {
157
+ // A failed backup still leaves the live file in place; the journal drop
158
+ // below is what keeps the pull from deleting it.
159
+ }
160
+ if (journalRows(run.journal).delete(relativePath))
161
+ flushShareJournal(run);
162
+ run.emit({
163
+ type: "error",
164
+ diagnostic: true,
165
+ path: relativePath,
166
+ message: `server has a newer deleted version of '${relativePath}', kept your copy` +
167
+ (backedUp ? " (backup in .hq/conflict-backups/)" : "") +
168
+ `. Restore the newer version with 'hq files restore --version-id', or delete your copy.`,
169
+ });
170
+ }
117
171
  /**
118
172
  * Local-only ephemeral artifacts: conflict-side files written whenever a
119
173
  * 3-way merge preserves the displaced side for inspection. Depending on the
@@ -1180,6 +1234,16 @@ async function runShareOnce(options, run) {
1180
1234
  return buildShareResult(run, counters, filesRefusedStalePaths, uploadResult.abortFlightConflictPaths, pathResults, true);
1181
1235
  }
1182
1236
  await executeDeletes(run, plans.deletePlan, plans.decommissionPlan, counters, filesRefusedStalePaths, pathResults, !uploadResult.hadUploadFailure && uploadResult.workerErrors.length === 0);
1237
+ if (run.writeForbiddenSet.size > REFUSED_STALE_PATH_CAP) {
1238
+ const more = run.writeForbiddenSet.size - REFUSED_STALE_PATH_CAP;
1239
+ run.emit({
1240
+ type: "error",
1241
+ diagnostic: true,
1242
+ path: "(write-forbidden)",
1243
+ message: `${more} more files were refused by the vault (no write access, server put denied: FILES_PRESIGN_FORBIDDEN); ` +
1244
+ `${run.writeForbiddenSet.size} refused in total, kept local-only`,
1245
+ });
1246
+ }
1183
1247
  finalizeShareJournal(run);
1184
1248
  throwUploadWorkerErrors(uploadResult.workerErrors, run.emit);
1185
1249
  // A reconcile cursor is an all-or-nothing boundary for this bounded apply
@@ -1587,6 +1651,7 @@ async function createPushRunContext(options) {
1587
1651
  threshold: checkpointThresholdFor(journal, options.durableFullPassProgress === true),
1588
1652
  baselineLastSync: journal.lastSync,
1589
1653
  error: null,
1654
+ unjournaledIntents: [],
1590
1655
  },
1591
1656
  };
1592
1657
  }
@@ -3056,6 +3121,12 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
3056
3121
  await restartForChangedBody(afterHead);
3057
3122
  return;
3058
3123
  }
3124
+ // The remote may be an upload this machine started whose journal row
3125
+ // never reached disk (the pass was killed after its PUT). Take it as the
3126
+ // baseline before classifying, so the next local edit is an ordinary
3127
+ // one-sided change instead of a conflict against our own write.
3128
+ if (remoteMeta && item.kind === "file")
3129
+ adoptOwnUploadIntent(run, item, remoteMeta);
3059
3130
  // Journal entry for this key (if any). Used both for 3-way conflict
3060
3131
  // classification and for the write fence below: Prefer last-synced
3061
3132
  // `remoteEtag` over live HEAD so a peer-advanced object cannot be
@@ -3164,8 +3235,10 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
3164
3235
  // Replaying that unchanged baseline is a stale revert, not an edit. A
3165
3236
  // user who deliberately restores some other historic content has a hash
3166
3237
  // different from the journal baseline and still reaches normal conflict
3167
- // resolution below.
3168
- if (journalEntry !== undefined &&
3238
+ // resolution below. A remote still at the journaled revision has not
3239
+ // superseded anything, whatever its published lineage says.
3240
+ if (remoteChanged &&
3241
+ journalEntry !== undefined &&
3169
3242
  journalEntry.localDiverges !== true &&
3170
3243
  remoteContentHash !== undefined &&
3171
3244
  journalEntry.hash === localHash &&
@@ -3540,7 +3613,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
3540
3613
  ? { ifMatch: journalEntry.remoteEtag }
3541
3614
  : { ifMatch: remoteMeta.etag }
3542
3615
  : { ifNoneMatch: "*" };
3543
- const performUpload = async (pc, observedRemote = remoteMeta) => {
3616
+ const performUploadOnce = async (pc, observedRemote) => {
3544
3617
  const isSymlinkUpload = item.kind === "symlink";
3545
3618
  const lstat = fs.lstatSync(absolutePath);
3546
3619
  const size = isSymlinkUpload ? 0 : lstat.size;
@@ -3767,6 +3840,9 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
3767
3840
  else {
3768
3841
  // Strictly after the PUT returned and the entry was applied, so an
3769
3842
  // interrupted pass can only ever re-upload a file, never skip one.
3843
+ // Until that checkpoint the upload's intent stays on disk, so a pass
3844
+ // killed before it cannot make the next pass take this object for a
3845
+ // peer's edit (feedback_a1696094).
3770
3846
  checkpointShareJournal(run);
3771
3847
  }
3772
3848
  if (skillContentAtPut !== undefined &&
@@ -3797,6 +3873,36 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
3797
3873
  transfer.complete();
3798
3874
  }
3799
3875
  };
3876
+ // The intent is on disk before the PUT leaves. A landed PUT keeps it until
3877
+ // the next journal write puts its row on disk (see writeShareJournal),
3878
+ // including when this call then fails on that very write. Only a PUT the
3879
+ // server refused (412), or one superseded before it was sent, is known not
3880
+ // to have landed and forgets it at once.
3881
+ // Any other failure (timeout, reset) may have landed, so the intent stays
3882
+ // for this process's life and ages out on disk. A process that dies with
3883
+ // intents outstanding leaves them for the next pass to recognise those
3884
+ // objects as this machine's own writes instead of a peer's edits.
3885
+ const performUpload = async (pc, observedRemote = remoteMeta) => {
3886
+ const intent = item.kind === "file"
3887
+ ? recordUploadIntent(run.journalSlug, relativePath, localHash)
3888
+ : undefined;
3889
+ const landedBefore = uploadCompleted;
3890
+ try {
3891
+ await performUploadOnce(pc, observedRemote);
3892
+ }
3893
+ catch (err) {
3894
+ if (intent) {
3895
+ if (uploadCompleted && !landedBefore)
3896
+ run.checkpoint.unjournaledIntents.push(intent);
3897
+ else if (isPreconditionFailed(err) || err instanceof SamePathUploadSupersededError) {
3898
+ settleUploadIntent(intent);
3899
+ }
3900
+ }
3901
+ throw err;
3902
+ }
3903
+ if (intent)
3904
+ run.checkpoint.unjournaledIntents.push(intent);
3905
+ };
3800
3906
  try {
3801
3907
  await performUpload(precondition);
3802
3908
  }
@@ -3962,6 +4068,12 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
3962
4068
  throw retryErr;
3963
4069
  if (emitPlanLimitSkip(retryErr, relativePath))
3964
4070
  return;
4071
+ if (isPresignDeletedNewer(retryErr)) {
4072
+ keepLocalOverDeletedNewer(run, item.absolutePath, relativePath);
4073
+ counters.filesSkipped++;
4074
+ counters.filesSkippedConflict++;
4075
+ return;
4076
+ }
3965
4077
  if (isPreconditionFailed(retryErr)) {
3966
4078
  counters.filesSkipped++;
3967
4079
  counters.filesSkippedConflict++;
@@ -4073,6 +4185,12 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
4073
4185
  }
4074
4186
  if (emitPlanLimitSkip(retryErr, relativePath))
4075
4187
  return;
4188
+ if (isPresignDeletedNewer(retryErr)) {
4189
+ keepLocalOverDeletedNewer(run, item.absolutePath, relativePath);
4190
+ counters.filesSkipped++;
4191
+ counters.filesSkippedConflict++;
4192
+ return;
4193
+ }
4076
4194
  hadUploadFailure = true;
4077
4195
  run.uploadFailedSet.add(relativePath);
4078
4196
  run.emit({
@@ -4093,18 +4211,35 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
4093
4211
  counters.filesSkippedConflict++;
4094
4212
  return;
4095
4213
  }
4214
+ if (isPresignDeletedNewer(err)) {
4215
+ // The server holds a NEWER version of this key behind a delete marker
4216
+ // (hq-pro refusalReason "deleted-newer", feedback_7b30e522). Keep the
4217
+ // local file, park one backup copy out of tree, and drop the journal
4218
+ // row so the pull leg sees an untracked local file instead of a
4219
+ // remote delete to apply. Not an upload failure: nothing was lost.
4220
+ keepLocalOverDeletedNewer(run, item.absolutePath, relativePath);
4221
+ counters.filesSkipped++;
4222
+ counters.filesSkippedConflict++;
4223
+ return;
4224
+ }
4096
4225
  if (isAccessDenied(err)) {
4097
4226
  hadUploadFailure = true;
4098
4227
  if (isPresignWriteForbidden(err)) {
4099
4228
  // A write denial on a key in this push: report it instead of
4100
4229
  // folding it into the silent scope-excluded count (feedback_682e9187).
4230
+ // Name the first REFUSED_STALE_PATH_CAP paths; the rest are folded
4231
+ // into one summary event after the uploads finish, so a member
4232
+ // without write access to a large folder does not get one line per
4233
+ // file on every pass (feedback_f84671f4).
4101
4234
  run.writeForbiddenSet.add(relativePath);
4102
- run.emit({
4103
- type: "error",
4104
- diagnostic: true,
4105
- path: relativePath,
4106
- message: describeError(err),
4107
- });
4235
+ if (run.writeForbiddenSet.size <= REFUSED_STALE_PATH_CAP) {
4236
+ run.emit({
4237
+ type: "error",
4238
+ diagnostic: true,
4239
+ path: relativePath,
4240
+ message: describeError(err),
4241
+ });
4242
+ }
4108
4243
  return;
4109
4244
  }
4110
4245
  run.scopeExcludedSet.add(relativePath);
@@ -4375,6 +4510,43 @@ function adoptLandedSamePathUpload(run, item, landed) {
4375
4510
  }
4376
4511
  return true;
4377
4512
  }
4513
+ /**
4514
+ * Take a remote revision as this run's baseline when its published content
4515
+ * hash matches an upload this machine recorded before a PUT whose journal row
4516
+ * never reached disk (the pass was killed in between). The object then holds
4517
+ * our own bytes, so the decision below sees an ordinary local edit, fenced on
4518
+ * the remote ETag, instead of a two-sided conflict that lets the cloud copy
4519
+ * replace the newer local file (feedback_a1696094).
4520
+ *
4521
+ * The row carries no local stat stamps: they would claim the local body still
4522
+ * hashes to the adopted value. Returns true when the row was replaced.
4523
+ */
4524
+ function adoptOwnUploadIntent(run, item, remoteMeta) {
4525
+ const remoteHash = publishedContentHash(remoteMeta.metadata);
4526
+ if (remoteHash === undefined || remoteHash === item.localHash)
4527
+ return false;
4528
+ const row = journalRows(run.journal).get(item.relativePath);
4529
+ if (row?.localDiverges === true || row?.hash === remoteHash)
4530
+ return false;
4531
+ const remoteEtag = normalizeEtag(remoteMeta.etag);
4532
+ if (row?.remoteEtag !== undefined && normalizeEtag(row.remoteEtag) === remoteEtag)
4533
+ return false;
4534
+ const rowWrittenAt = row ? Date.parse(row.syncedAt) : Number.NaN;
4535
+ const intent = findUploadIntent(run.journalSlug, item.relativePath, remoteHash, Number.isFinite(rowWrittenAt) ? rowWrittenAt : undefined);
4536
+ if (!intent)
4537
+ return false;
4538
+ try {
4539
+ updateEntry(run.journal, item.relativePath, remoteHash, remoteMeta.size, "up", item.absolutePath, {
4540
+ remoteEtag,
4541
+ kind: "file",
4542
+ });
4543
+ }
4544
+ catch {
4545
+ // The local path vanished; the normal replan settles it.
4546
+ return false;
4547
+ }
4548
+ return true;
4549
+ }
4378
4550
  /**
4379
4551
  * Exact-prefix and storage-class-specific, matching the pull planner: only
4380
4552
  * session transcripts in DEEP_ARCHIVE are frozen. Other archived objects keep
@@ -4694,7 +4866,11 @@ function recordPushConflictNotice(run, item, backupPath, sideKept, decision, rem
4694
4866
  companySlug: run.options.personalMode === true ? null : run.ctx.slug,
4695
4867
  relativePath: vaultKeyForLocalPath(run.hqRoot, item.absolutePath),
4696
4868
  backupPath,
4697
- winnerReason: remoteChangedSinceJournal
4869
+ // A remote kept over a NEWER local body is not "remote-newer"; the
4870
+ // decision names why it was kept.
4871
+ winnerReason: remoteChangedSinceJournal ||
4872
+ decision?.reason === "remote-changed" ||
4873
+ decision?.reason === "no-baseline"
4698
4874
  ? "remote-changed-since-journal"
4699
4875
  : sideKept === "local" ? "local-newer" : "remote-newer",
4700
4876
  sideKept,
@@ -4896,7 +5072,7 @@ async function adoptRemoteOnPush(run, item, remoteHash, decision, probe) {
4896
5072
  // Persist the converged remote-backed entry immediately. A conflict
4897
5073
  // resolution is a completed transfer even though the push counter is
4898
5074
  // recorded as skipped, and an interrupted pass must not revive it.
4899
- checkpointShareJournal(run);
5075
+ checkpointShareJournal(run, { persistRow: true });
4900
5076
  }
4901
5077
  catch (adoptErr) {
4902
5078
  if (parkAttempted && (!backupHash || backupHash !== item.localHash)) {
@@ -5310,25 +5486,34 @@ function checkpointThresholdFor(journal, durableFullPassProgress = false) {
5310
5486
  * Called after each result is applied to the in-memory journal. The entry is
5311
5487
  * written only AFTER its object PUT has returned, so a checkpoint can at worst
5312
5488
  * cause a file to be re-uploaded — never mark an unshipped file as shipped.
5489
+ *
5490
+ * `persistRow` writes the journal even between checkpoints, for a result that
5491
+ * changed the vault (an upload, an adopted conflict). The write appends only
5492
+ * the changed rows; the object-event flush and the checkpoint counters keep
5493
+ * their cadence.
5313
5494
  */
5314
- function checkpointShareJournal(run) {
5495
+ function checkpointShareJournal(run, { persistRow = false } = {}) {
5315
5496
  run.checkpoint.appliedSinceFlush += 1;
5316
5497
  const override = checkpointCadenceOverride();
5317
5498
  if (override !== null) {
5318
- if (run.checkpoint.appliedSinceFlush >= override)
5499
+ if (run.checkpoint.appliedSinceFlush >= override) {
5319
5500
  flushShareJournal(run);
5320
- return;
5501
+ return;
5502
+ }
5321
5503
  }
5322
- if (run.checkpoint.appliedSinceFlush < run.checkpoint.threshold)
5323
- return;
5324
- if (run.options.durableFullPassProgress === true) {
5325
- flushShareJournal(run);
5326
- return;
5504
+ else if (run.checkpoint.appliedSinceFlush >= run.checkpoint.threshold) {
5505
+ if (run.options.durableFullPassProgress === true) {
5506
+ flushShareJournal(run);
5507
+ return;
5508
+ }
5509
+ const now = Date.now();
5510
+ if (now - run.checkpoint.lastFlushAt >= CHECKPOINT_MIN_INTERVAL_MS) {
5511
+ flushShareJournal(run, now);
5512
+ return;
5513
+ }
5327
5514
  }
5328
- const now = Date.now();
5329
- if (now - run.checkpoint.lastFlushAt < CHECKPOINT_MIN_INTERVAL_MS)
5330
- return;
5331
- flushShareJournal(run, now);
5515
+ if (persistRow)
5516
+ writeShareJournal(run);
5332
5517
  }
5333
5518
  /**
5334
5519
  * Publish the journal as it currently stands, WITHOUT advancing `lastSync`.
@@ -5336,6 +5521,14 @@ function checkpointShareJournal(run) {
5336
5521
  * "these objects are shipped", not "this sync finished".
5337
5522
  */
5338
5523
  function flushShareJournal(run, now = Date.now()) {
5524
+ writeShareJournal(run);
5525
+ run.checkpoint.appliedSinceFlush = 0;
5526
+ run.checkpoint.lastFlushAt = now;
5527
+ void run.objectEvents.flush();
5528
+ run.checkpoint.threshold = checkpointThresholdFor(run.journal, run.options.durableFullPassProgress === true);
5529
+ }
5530
+ /** The journal write of {@link flushShareJournal}, without its bookkeeping. */
5531
+ function writeShareJournal(run) {
5339
5532
  const liveLastSync = run.journal.lastSync;
5340
5533
  run.journal.lastSync = run.checkpoint.baselineLastSync;
5341
5534
  const release = setJournalSnapshotQuarantineListener(run.journalSlug, (diagnostic) => run.emit({ type: "journal-quarantine", ...diagnostic }));
@@ -5350,10 +5543,16 @@ function flushShareJournal(run, now = Date.now()) {
5350
5543
  release();
5351
5544
  run.journal.lastSync = liveLastSync;
5352
5545
  }
5353
- run.checkpoint.appliedSinceFlush = 0;
5354
- run.checkpoint.lastFlushAt = now;
5355
- void run.objectEvents.flush();
5356
- run.checkpoint.threshold = checkpointThresholdFor(run.journal, run.options.durableFullPassProgress === true);
5546
+ settleJournaledIntents(run);
5547
+ }
5548
+ /** The rows of every landed upload are on disk now; their intents can go. */
5549
+ function settleJournaledIntents(run) {
5550
+ const settled = run.checkpoint.unjournaledIntents;
5551
+ if (settled.length === 0)
5552
+ return;
5553
+ run.checkpoint.unjournaledIntents = [];
5554
+ for (const intent of settled)
5555
+ settleUploadIntent(intent);
5357
5556
  }
5358
5557
  function finalizeShareJournal(run) {
5359
5558
  run.journal.lastSync = new Date().toISOString();
@@ -5364,6 +5563,7 @@ function finalizeShareJournal(run) {
5364
5563
  finally {
5365
5564
  release();
5366
5565
  }
5566
+ settleJournaledIntents(run);
5367
5567
  if (run.excludedSet.size > 0) {
5368
5568
  const samplePaths = [];
5369
5569
  for (const p of run.excludedSet) {
@@ -5608,6 +5808,7 @@ function buildShareResult(run, counters, filesRefusedStalePaths, conflictPaths,
5608
5808
  uploadFailedPaths: [...run.uploadFailedSet].slice(0, REFUSED_STALE_PATH_CAP),
5609
5809
  filesWriteForbidden: run.writeForbiddenSet.size,
5610
5810
  writeForbiddenPaths: [...run.writeForbiddenSet].slice(0, REFUSED_STALE_PATH_CAP),
5811
+ writeForbiddenHoldPaths: [...run.writeForbiddenSet],
5611
5812
  filesSuppressedByTombstone: counters.filesSuppressedByTombstone,
5612
5813
  ...(run.skillRegisterLandedBytes
5613
5814
  ? { skillRegistrationFallbacks: run.skillRegistrationFallbacks }