Skip to content

Commit b50bbe8

Browse files
committed
fix(knowledge): restart listings for Full resync and reset renewal on identity change
A Full resync now starts a new full-sync listing even when the connector does not rehydrate, so Slack rereads every thread instead of resuming an ordinary cursor. A member whose identity changes or whose token is rejected also loses its scope-renewal watermark, so renewal runs for the new identity right away.
1 parent cef0b6d commit b50bbe8

4 files changed

Lines changed: 41 additions & 1 deletion

File tree

‎apps/sim/lib/knowledge/connectors/member-sync-engine.integration.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ describe('member engine with a dedicated content credential', () => {
357357
subjectToken: 's:google-drive:-:different-person',
358358
memberSyncedThrough: null,
359359
lastCompleteListingAt: null,
360+
scopeRenewedAt: null,
360361
listingCheckpoint: { kind: 'membership', cursor: null, removeMember: false },
361362
changeCursor: null,
362363
nextAttemptAt: expect.any(Date),
@@ -704,6 +705,7 @@ describe('member engine with a dedicated content credential', () => {
704705
status: 'suspended',
705706
memberSyncedThrough: null,
706707
lastCompleteListingAt: null,
708+
scopeRenewedAt: null,
707709
listingCheckpoint: { kind: 'membership', cursor: null, removeMember: false },
708710
})
709711
)

‎apps/sim/lib/knowledge/connectors/member-sync-engine.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,7 @@ async function reconcileMembership(
778778
memberSyncedThrough: null,
779779
lastCompleteListingAt: null,
780780
lastListedCount: null,
781+
scopeRenewedAt: null,
781782
}
782783
: {}),
783784
updatedAt: now,
@@ -985,6 +986,7 @@ async function recordMemberFailure(
985986
changeCursor: null,
986987
memberSyncedThrough: null,
987988
lastCompleteListingAt: null,
989+
scopeRenewedAt: null,
988990
}
989991
: {}),
990992
consecutiveFailures: failures,

‎apps/sim/lib/knowledge/connectors/sync-content-pass.test.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ async function runPass(
286286
access?: ConnectorAccessMode
287287
readCurrent?: boolean
288288
forceRehydrate?: boolean
289+
fullSync?: boolean
289290
checkpoint?: ListingCheckpoint
290291
databaseTime?: Date
291292
getDocument?: () => Promise<ExternalDocument | null>
@@ -387,6 +388,7 @@ async function runPass(
387388
getAccessToken: async () => 'token',
388389
hydration: { getDocument: hydrate },
389390
forceRehydrate: options.forceRehydrate ?? false,
391+
fullSync: options.fullSync,
390392
deadlineAt: Date.now() + 60_000,
391393
onPage: mocks.onPage,
392394
})
@@ -601,6 +603,36 @@ describe('content pass checkpoint intent', () => {
601603
}
602604
)
603605

606+
it('restarts an ordinary checkpoint as a full sync when Full resync is requested without rehydration', async () => {
607+
const checkpoint = {
608+
...beginListingCheckpoint({
609+
fingerprint: 'a'.repeat(64),
610+
generationId: 'previous-run',
611+
startedAt: new Date('2026-09-08T11:00:00Z'),
612+
}),
613+
cursor: 'saved-cursor',
614+
listedCount: 1,
615+
}
616+
const { pass, listDocuments } = await runPass({
617+
existing: {
618+
...EXISTING,
619+
contentHash: 'confluence:view-text-v2:page:3',
620+
storageKey: 'kb/old.txt',
621+
sourceSeenAt: new Date(checkpoint.startedAt),
622+
},
623+
checkpoint,
624+
fullSync: true,
625+
})
626+
627+
expect(listDocuments.mock.calls[0]?.[2]).toBeUndefined()
628+
expect(listDocuments.mock.calls[0]?.[3]).toMatchObject({ fullSync: true })
629+
expect(pass.checkpoint).toMatchObject({
630+
generationId: 'run',
631+
forceRehydrate: false,
632+
fullSync: true,
633+
})
634+
})
635+
604636
it.each([
605637
{ name: 'ordinary automatic retry', savedRehydrate: false, requestedRehydrate: false },
606638
{ name: 'Full resync automatic retry', savedRehydrate: true, requestedRehydrate: false },

‎apps/sim/lib/knowledge/connectors/sync-content-pass.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ export async function runConnectorContentPass(input: ContentPassInput) {
101101
}
102102
let checkpoint = readListingCheckpoint(input.connector.listingCheckpoint, input.fingerprint)
103103
/** A Full resync must revisit documents before an ordinary listing's saved cursor. */
104-
if (!checkpoint || (input.forceRehydrate && !checkpoint.forceRehydrate)) {
104+
if (
105+
!checkpoint ||
106+
(input.forceRehydrate && !checkpoint.forceRehydrate) ||
107+
(input.fullSync && !checkpoint.fullSync)
108+
) {
105109
checkpoint = await withLease(async (tx) => {
106110
const next = beginListingCheckpoint({
107111
fingerprint: input.fingerprint,

0 commit comments

Comments
 (0)