From b7247c499bf71c5120218b2b025d32b37635aa8f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 24 Oct 2026 11:33:56 -0700 Subject: [PATCH] fix(gmail): clear the history cursor only from a poll that held one A search-mode poll (no stored cursor) sent historyId: undefined, which updateWebhookProviderConfig treats as a key removal. If it overlapped a newer poll that had just stored a cursor, it deleted that cursor. The poll state is now built once and carries historyId only when this poll started with a cursor or found one, so only a poll whose cursor Gmail rejected can clear it. --- apps/sim/lib/webhooks/polling/gmail.test.ts | 12 ++++++++++- apps/sim/lib/webhooks/polling/gmail.ts | 23 +++++++-------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/apps/sim/lib/webhooks/polling/gmail.test.ts b/apps/sim/lib/webhooks/polling/gmail.test.ts index 36de4423dc9..c1394a9dc82 100644 --- a/apps/sim/lib/webhooks/polling/gmail.test.ts +++ b/apps/sim/lib/webhooks/polling/gmail.test.ts @@ -136,7 +136,7 @@ function persistedHistoryIds() { ) } -describe('gmailPollingHandler expired history cursor', () => { +describe('gmailPollingHandler history cursor', () => { beforeEach(() => { mockResolveOAuthCredential.mockResolvedValue('access-token') mockProcessEvent.mockResolvedValue({ success: true }) @@ -180,4 +180,14 @@ describe('gmailPollingHandler expired history cursor', () => { expect(persistedHistoryIds()).toEqual(['40100']) }) + + it('leaves the stored cursor untouched when a search-mode poll finds no mail', async () => { + mailboxWithExpiredHistory({ profileHistoryId: '40100', inboxMessages: [] }) + + await expect(gmailPollingHandler.pollWebhook(context({}))).resolves.toBe('success') + + expect(mockUpdateConfig).toHaveBeenCalledTimes(1) + const [, updates] = mockUpdateConfig.mock.calls[0] + expect(updates).not.toHaveProperty('historyId') + }) }) diff --git a/apps/sim/lib/webhooks/polling/gmail.ts b/apps/sim/lib/webhooks/polling/gmail.ts index 780b911938e..97514ef5646 100644 --- a/apps/sim/lib/webhooks/polling/gmail.ts +++ b/apps/sim/lib/webhooks/polling/gmail.ts @@ -83,16 +83,14 @@ export const gmailPollingHandler: PollingProviderHandler = { requestId, logger ) + const pollState = { + lastCheckedTimestamp: now.toISOString(), + // Omit unless this poll held a cursor, so it can't clear one an overlapping poll stored. + ...(config.historyId || latestHistoryId ? { historyId: latestHistoryId } : {}), + } if (!emails || !emails.length) { - await updateWebhookProviderConfig( - webhookId, - { - lastCheckedTimestamp: now.toISOString(), - historyId: latestHistoryId, - }, - logger - ) + await updateWebhookProviderConfig(webhookId, pollState, logger) await markWebhookSuccess(webhookId, logger) logger.info(`[${requestId}] No new emails found for webhook ${webhookId}`) return 'success' @@ -110,14 +108,7 @@ export const gmailPollingHandler: PollingProviderHandler = { logger ) - await updateWebhookProviderConfig( - webhookId, - { - lastCheckedTimestamp: now.toISOString(), - historyId: latestHistoryId, - }, - logger - ) + await updateWebhookProviderConfig(webhookId, pollState, logger) if (failedCount > 0 && processedCount === 0) { await markWebhookFailed(webhookId, logger)