@@ -128,6 +128,27 @@ async function completeSyncLog(
128128 . where ( eq ( knowledgeConnectorSyncLog . id , syncLogId ) )
129129}
130130
131+ /**
132+ * Decides whether deletion reconciliation may run for a sync.
133+ *
134+ * Reconciliation hard-deletes every stored document absent from the listing,
135+ * so it must only run against a complete source set:
136+ * - never on incremental syncs (they list only changed documents)
137+ * - never when the engine truncated pagination (`listingTruncated`) — a forced
138+ * fullSync cannot fix truncation, so it cannot override it
139+ * - not when a connector capped its listing (`listingCapped`), unless a forced
140+ * fullSync deliberately overrides the cap to reconcile the capped scope
141+ */
142+ export function shouldReconcileDeletions (
143+ isIncremental : boolean | undefined ,
144+ syncContext : Record < string , unknown > | undefined ,
145+ fullSync : boolean | undefined
146+ ) : boolean {
147+ if ( isIncremental ) return false
148+ if ( syncContext ?. listingTruncated ) return false
149+ return ! syncContext ?. listingCapped || Boolean ( fullSync )
150+ }
151+
131152/**
132153 * Resolves tag values from connector metadata using the connector's mapTags function.
133154 * Translates semantic keys returned by mapTags to actual DB slots using the
@@ -417,17 +438,11 @@ export async function executeSync(
417438
418439 if ( hasMore ) {
419440 /**
420- * Pagination stopped before the source was exhausted — either the
421- * MAX_PAGES guard tripped or the connector reported hasMore without a
422- * cursor. The listing is incomplete, so flag it to suppress deletion
423- * reconciliation; otherwise documents beyond the truncation point would
424- * be removed even though they still exist in the source.
425- *
426- * `listingTruncated` is distinct from connector-set `listingCapped`:
427- * a forced fullSync may legitimately override a connector's soft cap
428- * (the user opted to reconcile the capped scope), but engine-level
429- * truncation can never be resolved by forcing a fullSync — the next
430- * fullSync truncates identically — so it is an absolute deletion block.
441+ * Pagination stopped before source exhaustion (MAX_PAGES or a missing
442+ * cursor), so the listing is incomplete. `listingTruncated` blocks
443+ * deletion reconciliation absolutely — unlike connector-set
444+ * `listingCapped`, it cannot be overridden by a forced fullSync, since
445+ * re-running one truncates identically.
431446 */
432447 syncContext . listingCapped = true
433448 syncContext . listingTruncated = true
@@ -657,15 +672,7 @@ export async function executeSync(
657672 }
658673 }
659674
660- // Reconcile deletions for non-incremental syncs that returned ALL docs.
661- // Skip when listing was capped (maxFiles/maxThreads) — unseen docs may still exist in the source.
662- // A forced fullSync overrides connector-set caps, but never engine-level truncation
663- // (listingTruncated): a truncated listing is incomplete no matter how the sync was triggered.
664- if (
665- ! isIncremental &&
666- ! syncContext ?. listingTruncated &&
667- ( ! syncContext ?. listingCapped || options ?. fullSync )
668- ) {
675+ if ( shouldReconcileDeletions ( isIncremental , syncContext , options ?. fullSync ) ) {
669676 const removedIds = existingDocs
670677 . filter ( ( d ) => d . externalId && ! seenExternalIds . has ( d . externalId ) )
671678 . map ( ( d ) => d . id )
0 commit comments