From fb926996fa3ff8f824f2972212656b9dd77d23ec Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Apr 2026 09:32:09 -0700 Subject: [PATCH 1/6] fix(kb): improve error logging when connector token resolution fails The generic "Failed to obtain access token" error hid the actual root cause. Now logs credentialId, userId, authMode, and provider to help diagnose token refresh failures in trigger.dev. Co-Authored-By: Claude Opus 4.6 --- .../lib/knowledge/connectors/sync-engine.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/apps/sim/lib/knowledge/connectors/sync-engine.ts b/apps/sim/lib/knowledge/connectors/sync-engine.ts index 0b545516a3b..420a7f7d1ed 100644 --- a/apps/sim/lib/knowledge/connectors/sync-engine.ts +++ b/apps/sim/lib/knowledge/connectors/sync-engine.ts @@ -230,7 +230,7 @@ async function resolveAccessToken( connector: { credentialId: string | null; encryptedApiKey: string | null }, connectorConfig: { auth: ConnectorAuthConfig }, userId: string -): Promise { +): Promise { if (connectorConfig.auth.mode === 'apiKey') { if (!connector.encryptedApiKey) { throw new Error('API key connector is missing encrypted API key') @@ -243,11 +243,22 @@ async function resolveAccessToken( throw new Error('OAuth connector is missing credential ID') } - return refreshAccessTokenIfNeeded( - connector.credentialId, - userId, - `sync-${connector.credentialId}` - ) + const requestId = `sync-${connector.credentialId}` + const token = await refreshAccessTokenIfNeeded(connector.credentialId, userId, requestId) + + if (!token) { + logger.error(`[${requestId}] refreshAccessTokenIfNeeded returned null`, { + credentialId: connector.credentialId, + userId, + authMode: connectorConfig.auth.mode, + authProvider: connectorConfig.auth.provider, + }) + throw new Error( + `Failed to obtain access token for credential ${connector.credentialId} (provider: ${connectorConfig.auth.provider})` + ) + } + + return token } /** @@ -307,10 +318,6 @@ export async function executeSync( let accessToken = await resolveAccessToken(connector, connectorConfig, userId) - if (!accessToken) { - throw new Error('Failed to obtain access token') - } - const lockResult = await db .update(knowledgeConnector) .set({ status: 'syncing', updatedAt: new Date() }) From e66d4e54fd5ddcc0bc00998cac25f760e48212ae Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Apr 2026 09:44:06 -0700 Subject: [PATCH 2/6] feat(kb): disable connectors after 10 consecutive sync failures Connectors that fail 10 times in a row are set to 'disabled' status, stopping the cron from scheduling further syncs. The UI shows an alert triangle with a reconnect banner. Users can re-enable via the play button or by reconnecting their account, which resets failures. Co-Authored-By: Claude Opus 4.6 --- .../[id]/connectors/[connectorId]/route.ts | 12 ++++ .../connectors-section/connectors-section.tsx | 59 +++++++++++++++++-- apps/sim/hooks/queries/kb/connectors.ts | 2 +- .../lib/knowledge/connectors/sync-engine.ts | 17 +++++- 4 files changed, 81 insertions(+), 9 deletions(-) diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts index 0780f8453d7..8a62b8a3093 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts @@ -222,6 +222,18 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { } if (parsed.data.status !== undefined) { updates.status = parsed.data.status + if (parsed.data.status === 'active') { + updates.consecutiveFailures = 0 + updates.lastSyncError = null + if (!updates.nextSyncAt) { + const interval = parsed.data.syncIntervalMinutes + if (interval && interval > 0) { + updates.nextSyncAt = new Date(Date.now() + interval * 60 * 1000) + } else { + updates.nextSyncAt = new Date() + } + } + } } await db diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx index 3d08cdb7c62..ef40633723f 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx @@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger' import { format, formatDistanceToNow, isPast } from 'date-fns' import { AlertCircle, + AlertTriangle, CheckCircle2, ChevronDown, Loader2, @@ -66,6 +67,7 @@ const STATUS_CONFIG = { syncing: { label: 'Syncing', variant: 'amber' as const }, error: { label: 'Error', variant: 'red' as const }, paused: { label: 'Paused', variant: 'gray' as const }, + disabled: { label: 'Disabled', variant: 'red' as const }, } as const export function ConnectorsSection({ @@ -159,7 +161,10 @@ export function ConnectorsSection({ knowledgeBaseId, connectorId: connector.id, updates: { - status: connector.status === 'paused' ? 'active' : 'paused', + status: + connector.status === 'paused' || connector.status === 'disabled' + ? 'active' + : 'paused', }, }, { @@ -352,7 +357,12 @@ function ConnectorCard({
- {Icon && } +
+ {Icon && } + {connector.status === 'disabled' && ( + + )} +
@@ -441,7 +451,7 @@ function ConnectorCard({ > {isUpdating ? ( - ) : connector.status === 'paused' ? ( + ) : connector.status === 'paused' || connector.status === 'disabled' ? ( ) : ( @@ -449,7 +459,9 @@ function ConnectorCard({ - {connector.status === 'paused' ? 'Resume' : 'Pause'} + {connector.status === 'paused' || connector.status === 'disabled' + ? 'Resume' + : 'Pause'} @@ -481,7 +493,44 @@ function ConnectorCard({
- {missingScopes.length > 0 && ( + {connector.status === 'disabled' && ( +
+
+
+ + Connector disabled after repeated sync failures +
+

+ Syncing has been paused due to {connector.consecutiveFailures} consecutive failures. + Reconnect your account to resume syncing. +

+ {canEdit && ( + + )} +
+
+ )} + + {missingScopes.length > 0 && connector.status !== 'disabled' && (
diff --git a/apps/sim/hooks/queries/kb/connectors.ts b/apps/sim/hooks/queries/kb/connectors.ts index ae53f271640..c8a528cccc7 100644 --- a/apps/sim/hooks/queries/kb/connectors.ts +++ b/apps/sim/hooks/queries/kb/connectors.ts @@ -12,7 +12,7 @@ export interface ConnectorData { sourceConfig: Record syncMode: string syncIntervalMinutes: number - status: 'active' | 'paused' | 'syncing' | 'error' + status: 'active' | 'paused' | 'syncing' | 'error' | 'disabled' lastSyncAt: string | null lastSyncError: string | null lastSyncDocCount: number | null diff --git a/apps/sim/lib/knowledge/connectors/sync-engine.ts b/apps/sim/lib/knowledge/connectors/sync-engine.ts index 420a7f7d1ed..9e31f4b3021 100644 --- a/apps/sim/lib/knowledge/connectors/sync-engine.ts +++ b/apps/sim/lib/knowledge/connectors/sync-engine.ts @@ -46,6 +46,7 @@ const MAX_PAGES = 500 const MAX_SAFE_TITLE_LENGTH = 200 const STALE_PROCESSING_MINUTES = 45 const RETRY_WINDOW_DAYS = 7 +const MAX_CONSECUTIVE_FAILURES = 10 /** Sanitizes a document title for use in S3 storage keys. */ function sanitizeStorageTitle(title: string): string { @@ -796,15 +797,25 @@ export async function executeSync( const now = new Date() const failures = (connector.consecutiveFailures ?? 0) + 1 + const disabled = failures >= MAX_CONSECUTIVE_FAILURES const backoffMinutes = Math.min(failures * 30, 1440) - const nextSync = new Date(now.getTime() + backoffMinutes * 60 * 1000) + const nextSync = disabled ? null : new Date(now.getTime() + backoffMinutes * 60 * 1000) + + if (disabled) { + logger.warn('Connector disabled after repeated failures', { + connectorId, + consecutiveFailures: failures, + }) + } await db .update(knowledgeConnector) .set({ - status: 'error', + status: disabled ? 'disabled' : 'error', lastSyncAt: now, - lastSyncError: errorMessage, + lastSyncError: disabled + ? 'Connector disabled after repeated sync failures. Please reconnect.' + : errorMessage, nextSyncAt: nextSync, consecutiveFailures: failures, updatedAt: now, From 6b78d23de93c52ad1696c080f88503a3409c2246 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Apr 2026 09:46:30 -0700 Subject: [PATCH 3/6] fix(kb): disable sync button for disabled connectors, use amber badge variant Sync button should be disabled when connector is in disabled state to guide users toward reconnecting first. Badge variant changed from red to amber to match the warning banner styling. Co-Authored-By: Claude Opus 4.6 --- .../components/connectors-section/connectors-section.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx index ef40633723f..37d562e6450 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx @@ -67,7 +67,7 @@ const STATUS_CONFIG = { syncing: { label: 'Syncing', variant: 'amber' as const }, error: { label: 'Error', variant: 'red' as const }, paused: { label: 'Paused', variant: 'gray' as const }, - disabled: { label: 'Disabled', variant: 'red' as const }, + disabled: { label: 'Disabled', variant: 'amber' as const }, } as const export function ConnectorsSection({ @@ -417,7 +417,12 @@ function ConnectorCard({ variant='ghost' className='h-7 w-7 p-0' onClick={onSync} - disabled={connector.status === 'syncing' || isSyncPending || syncCooldown} + disabled={ + connector.status === 'syncing' || + connector.status === 'disabled' || + isSyncPending || + syncCooldown + } > Date: Wed, 8 Apr 2026 10:00:44 -0700 Subject: [PATCH 4/6] fix(kb): address PR review comments for disabled connector feature - Use `=== undefined` instead of falsy check for nextSyncAt to preserve explicit null (manual sync only) when syncIntervalMinutes is 0 - Gate Reconnect button on serviceId/providerId so it only renders for OAuth connectors; show appropriate copy for API key connectors Co-Authored-By: Claude Opus 4.6 --- .../api/knowledge/[id]/connectors/[connectorId]/route.ts | 2 +- .../components/connectors-section/connectors-section.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts index 8a62b8a3093..3aff3f66e5b 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts @@ -225,7 +225,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { if (parsed.data.status === 'active') { updates.consecutiveFailures = 0 updates.lastSyncError = null - if (!updates.nextSyncAt) { + if (updates.nextSyncAt === undefined) { const interval = parsed.data.syncIntervalMinutes if (interval && interval > 0) { updates.nextSyncAt = new Date(Date.now() + interval * 60 * 1000) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx index 37d562e6450..1445218a646 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx @@ -507,9 +507,11 @@ function ConnectorCard({

Syncing has been paused due to {connector.consecutiveFailures} consecutive failures. - Reconnect your account to resume syncing. + {serviceId + ? ' Reconnect your account to resume syncing.' + : ' Use the resume button to re-enable syncing.'}

- {canEdit && ( + {canEdit && serviceId && providerId && (