Skip to content

Commit 4d84cb6

Browse files
committed
fix(connectors): tighten listingCapped semantics per review (WIQL cap, batch omissions, cap-vs-exhaustion)
1 parent b8f0dd5 commit 4d84cb6

6 files changed

Lines changed: 95 additions & 28 deletions

File tree

apps/docs/app/global.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,13 @@ figure[data-rehype-pretty-code-figure],
510510
max-width: 480px !important;
511511
}
512512

513+
/* Search dialog overlay + panel must cover the sticky navbar — both default to z-50,
514+
and the navbar wins the tie by DOM order, leaving it unblurred above the overlay */
515+
.bg-fd-overlay,
516+
[role="dialog"][data-state] {
517+
z-index: 60 !important;
518+
}
519+
513520
pre {
514521
font-size: 0.875rem;
515522
line-height: 1.7;

apps/sim/connectors/azure-devops/azure-devops.ts

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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
*/
10111022
function 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)

apps/sim/connectors/google-forms/google-forms.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -550,9 +550,13 @@ export const googleFormsConnector: ConnectorConfig = {
550550
const data = await response.json()
551551
let files = (data.files || []) as DriveFormFile[]
552552

553+
let slicedSome = false
553554
if (maxForms > 0) {
554555
const remaining = maxForms - previouslyFetched
555-
if (files.length > remaining) files = files.slice(0, remaining)
556+
if (files.length > remaining) {
557+
slicedSome = true
558+
files = files.slice(0, remaining)
559+
}
556560
}
557561

558562
/**
@@ -597,11 +601,13 @@ export const googleFormsConnector: ConnectorConfig = {
597601
/**
598602
* Mark the listing as incomplete so the sync engine skips deletion
599603
* reconciliation. This applies when the `maxForms` cap truncates results
600-
* (forms beyond the cap are not absent from the source) or when a transient
601-
* error caused a still-present form to be dropped from this page — deleting
602-
* those would wipe valid documents from the knowledge base.
604+
* while more forms exist (forms beyond the cap are not absent from the
605+
* source) or when a transient error caused a still-present form to be
606+
* dropped from this page — deleting those would wipe valid documents from
607+
* the knowledge base. When the cap merely coincides with source exhaustion
608+
* (no next page), reconciliation stays enabled so deleted forms are cleaned up.
603609
*/
604-
if (syncContext && (hitLimit || skippedOnError)) {
610+
if (syncContext && ((hitLimit && (slicedSome || Boolean(nextPageToken))) || skippedOnError)) {
605611
syncContext.listingCapped = true
606612
}
607613

apps/sim/connectors/s3/s3.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,17 +581,20 @@ export const s3Connector: ConnectorConfig = {
581581
.filter((entry) => entry.size > 0 && entry.size <= MAX_FILE_SIZE)
582582
.map((entry) => objectToStub(ctx, entry))
583583

584+
let slicedSome = false
584585
if (maxObjects > 0) {
585586
const remaining = maxObjects - previouslyFetched
586587
if (documents.length > remaining) {
588+
slicedSome = true
587589
documents = documents.slice(0, remaining)
588590
}
589591
}
590592

591593
const totalFetched = previouslyFetched + documents.length
592594
if (syncContext) syncContext.totalDocsFetched = totalFetched
593595
const hitLimit = maxObjects > 0 && totalFetched >= maxObjects
594-
if (hitLimit && syncContext) syncContext.listingCapped = true
596+
const moreAvailable = slicedSome || (isTruncated && Boolean(nextContinuationToken))
597+
if (hitLimit && moreAvailable && syncContext) syncContext.listingCapped = true
595598

596599
return {
597600
documents,

apps/sim/connectors/sentry/sentry.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,19 +525,23 @@ export const sentryConnector: ConnectorConfig = {
525525

526526
const prevFetched = (syncContext?.totalDocsFetched as number) ?? 0
527527
let documents = issues.map(issueToStub)
528+
let slicedSome = false
528529
if (maxIssues > 0) {
529530
const remaining = Math.max(0, maxIssues - prevFetched)
530531
if (documents.length > remaining) {
532+
slicedSome = true
531533
documents = documents.slice(0, remaining)
532534
}
533535
}
534536

535537
const totalFetched = prevFetched + documents.length
536538
if (syncContext) syncContext.totalDocsFetched = totalFetched
537539
const hitLimit = maxIssues > 0 && totalFetched >= maxIssues
538-
if (hitLimit && syncContext) syncContext.listingCapped = true
539540

540541
const nextCursor = parseNextCursor(response.headers.get('Link'))
542+
if (hitLimit && (slicedSome || Boolean(nextCursor)) && syncContext) {
543+
syncContext.listingCapped = true
544+
}
541545
const hasMore = !hitLimit && Boolean(nextCursor)
542546

543547
return {

apps/sim/connectors/typeform/typeform.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -424,9 +424,13 @@ export const typeformConnector: ConnectorConfig = {
424424
* cap is hit because `hasMore` becomes false.
425425
*/
426426
let cappedItems = items
427+
let slicedSome = false
427428
if (maxResponses > 0) {
428429
const remaining = Math.max(0, maxResponses - prevTotal)
429-
if (items.length > remaining) cappedItems = items.slice(0, remaining)
430+
if (items.length > remaining) {
431+
slicedSome = true
432+
cappedItems = items.slice(0, remaining)
433+
}
430434
}
431435

432436
const documents: ExternalDocument[] = cappedItems.map((item) =>
@@ -437,13 +441,6 @@ export const typeformConnector: ConnectorConfig = {
437441
if (syncContext) syncContext.totalDocsFetched = totalFetched
438442
const hitLimit = maxResponses > 0 && totalFetched >= maxResponses
439443

440-
/**
441-
* Signal a truncated listing so the engine skips deletion reconciliation:
442-
* a capped page does not represent the full set of responses, and deleting
443-
* everything past the cap would wipe still-present docs from the KB.
444-
*/
445-
if (hitLimit && syncContext) syncContext.listingCapped = true
446-
447444
/**
448445
* The `before` cursor is the response `token` (not `response_id`). Each full
449446
* page advances to the oldest token seen so the next request pages strictly
@@ -452,7 +449,19 @@ export const typeformConnector: ConnectorConfig = {
452449
*/
453450
const lastItem = items[items.length - 1]
454451
const nextCursor = lastItem?.token
455-
const hasMore = !hitLimit && items.length === RESPONSES_PER_PAGE && Boolean(nextCursor)
452+
const sourceHasMore = items.length === RESPONSES_PER_PAGE && Boolean(nextCursor)
453+
454+
/**
455+
* Signal a truncated listing so the engine skips deletion reconciliation —
456+
* but only when the cap actually dropped responses (this page was sliced, or
457+
* the source had more pages). If the cap merely coincides with source
458+
* exhaustion, reconciliation stays enabled so deleted responses are cleaned up.
459+
*/
460+
if (hitLimit && (slicedSome || sourceHasMore) && syncContext) {
461+
syncContext.listingCapped = true
462+
}
463+
464+
const hasMore = !hitLimit && sourceHasMore
456465

457466
return {
458467
documents,

0 commit comments

Comments
 (0)