@@ -871,9 +871,14 @@ async function listRepoFiles(
871871 const chunk = entries . slice ( offset , offset + FILE_BATCH_SIZE )
872872 const documents = chunk . map ( ( entry ) => fileToStub ( organization , project , entry ) )
873873
874- const { documents : capped , capped : hitLimit } = applyMaxItemsCap ( documents , maxItems , syncContext )
875-
876874 const nextOffset = offset + FILE_BATCH_SIZE
875+ const { documents : capped , capped : hitLimit } = applyMaxItemsCap (
876+ documents ,
877+ maxItems ,
878+ syncContext ,
879+ nextOffset < entries . length
880+ )
881+
877882 const hasMore = ! hitLimit && nextOffset < entries . length
878883
879884 return {
@@ -1003,24 +1008,32 @@ async function getFileDocument(
10031008
10041009/**
10051010 * Applies the optional maxItems cap to a batch, tracking the running total in
1006- * syncContext and flagging `listingCapped` when the cap is hit. The sync engine
1007- * reads `listingCapped` to suppress deletion reconciliation on a truncated
1008- * listing — without it, a capped full sync would wrongly delete every source
1009- * document beyond the cap.
1011+ * syncContext and flagging `listingCapped` when the cap actually truncated the
1012+ * listing. The sync engine reads `listingCapped` to suppress deletion
1013+ * reconciliation on a truncated listing — without it, a capped full sync would
1014+ * wrongly delete every source document beyond the cap.
1015+ *
1016+ * `moreAvailable` tells the helper whether the current phase has further items
1017+ * beyond this page. The flag is only set when documents were actually dropped
1018+ * (this page was sliced, or more pages exist) — when the cap merely coincides
1019+ * with source exhaustion, reconciliation stays enabled so deleted source
1020+ * documents are still cleaned up.
10101021 */
10111022function applyMaxItemsCap (
10121023 documents : ExternalDocument [ ] ,
10131024 maxItems : number ,
1014- syncContext : Record < string , unknown > | undefined
1025+ syncContext : Record < string , unknown > | undefined ,
1026+ moreAvailable : boolean
10151027) : { documents : ExternalDocument [ ] ; capped : boolean } {
10161028 if ( maxItems <= 0 ) return { documents, capped : false }
10171029 const prevTotal = ( syncContext ?. totalDocsFetched as number ) ?? 0
10181030 const remaining = Math . max ( 0 , maxItems - prevTotal )
1019- const sliced = documents . length > remaining ? documents . slice ( 0 , remaining ) : documents
1031+ const slicedSome = documents . length > remaining
1032+ const sliced = slicedSome ? documents . slice ( 0 , remaining ) : documents
10201033 const newTotal = prevTotal + sliced . length
10211034 if ( syncContext ) syncContext . totalDocsFetched = newTotal
10221035 const capped = newTotal >= maxItems
1023- if ( capped && syncContext ) syncContext . listingCapped = true
1036+ if ( capped && ( slicedSome || moreAvailable ) && syncContext ) syncContext . listingCapped = true
10241037 return { documents : sliced , capped }
10251038}
10261039
@@ -1104,7 +1117,12 @@ async function listWikiPages(
11041117 documents . push ( ...stubs )
11051118 }
11061119
1107- const { documents : capped , capped : hitLimit } = applyMaxItemsCap ( documents , maxItems , syncContext )
1120+ const { documents : capped , capped : hitLimit } = applyMaxItemsCap (
1121+ documents ,
1122+ maxItems ,
1123+ syncContext ,
1124+ Boolean ( nextContinuation ) || wikiIndex + 1 < wikis . length
1125+ )
11081126 if ( hitLimit ) {
11091127 return { documents : capped , hasMore : false }
11101128 }
@@ -1145,6 +1163,10 @@ async function listWorkItems(
11451163 if ( syncContext ) syncContext . workItemIds = ids
11461164 }
11471165
1166+ if ( ids . length >= WIQL_MAX_RESULTS && syncContext ) {
1167+ syncContext . listingCapped = true
1168+ }
1169+
11481170 if ( ids . length === 0 ) {
11491171 return { documents : [ ] , hasMore : false }
11501172 }
@@ -1157,11 +1179,23 @@ async function listWorkItems(
11571179
11581180 const chunk = ids . slice ( offset , offset + WORK_ITEM_BATCH_SIZE )
11591181 const raw = await fetchWorkItemsBatch ( accessToken , organization , project , chunk )
1182+ if ( raw . length < chunk . length && syncContext ) {
1183+ syncContext . listingCapped = true
1184+ logger . warn (
1185+ 'workitemsbatch omitted ids that WIQL returned; flagging listing as incomplete so reconciliation skips deletion' ,
1186+ { requested : chunk . length , returned : raw . length , organization, project }
1187+ )
1188+ }
11601189 const documents = raw . map ( ( item ) => workItemToDocument ( organization , project , item ) )
11611190
1162- const { documents : capped , capped : hitLimit } = applyMaxItemsCap ( documents , maxItems , syncContext )
1163-
11641191 const nextOffset = offset + WORK_ITEM_BATCH_SIZE
1192+ const { documents : capped , capped : hitLimit } = applyMaxItemsCap (
1193+ documents ,
1194+ maxItems ,
1195+ syncContext ,
1196+ nextOffset < ids . length
1197+ )
1198+
11651199 const hasMore = ! hitLimit && nextOffset < ids . length
11661200
11671201 return {
@@ -1426,6 +1460,10 @@ export const azureDevopsConnector: ConnectorConfig = {
14261460 return { documents, nextCursor : result . nextCursor , hasMore : true }
14271461 }
14281462 if ( capReached ( ) ) {
1463+ const remainingPhase = nextPhase ( current , contentType )
1464+ if ( remainingPhase && syncContext ) {
1465+ syncContext . listingCapped = true
1466+ }
14291467 return { documents, hasMore : false }
14301468 }
14311469 current = nextPhase ( current , contentType )
0 commit comments