From 8e164f608c2c9e4c96ac8adca6b6ccb9d0343013 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Wed, 25 Feb 2026 00:23:11 +0200 Subject: [PATCH] fix(add-gmail): graceful startup when credentials missing + poll backoff - connect() warns and returns instead of throwing when ~/.gmail-mcp/ credentials are missing, preventing app crash - index.ts wraps gmail.connect() in try/catch as a safety net - Poll loop uses exponential backoff on consecutive errors (caps at 30m) instead of hammering the Gmail API every 60s on auth failures - Switch from setInterval to setTimeout chain for proper backoff timing Co-Authored-By: Claude Opus 4.6 --- .../add-gmail/add/src/channels/gmail.ts | 38 ++++++++++++------- .claude/skills/add-gmail/modify/src/index.ts | 6 ++- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/.claude/skills/add-gmail/add/src/channels/gmail.ts b/.claude/skills/add-gmail/add/src/channels/gmail.ts index 4d11bdf..b9ade60 100644 --- a/.claude/skills/add-gmail/add/src/channels/gmail.ts +++ b/.claude/skills/add-gmail/add/src/channels/gmail.ts @@ -34,9 +34,10 @@ export class GmailChannel implements Channel { private gmail: gmail_v1.Gmail | null = null; private opts: GmailChannelOpts; private pollIntervalMs: number; - private pollTimer: ReturnType | null = null; + private pollTimer: ReturnType | null = null; private processedIds = new Set(); private threadMeta = new Map(); + private consecutiveErrors = 0; private userEmail = ''; constructor(opts: GmailChannelOpts, pollIntervalMs = 60000) { @@ -50,9 +51,10 @@ export class GmailChannel implements Channel { const tokensPath = path.join(credDir, 'credentials.json'); if (!fs.existsSync(keysPath) || !fs.existsSync(tokensPath)) { - throw new Error( - 'Gmail credentials not found in ~/.gmail-mcp/. Run Gmail OAuth setup first.', + logger.warn( + 'Gmail credentials not found in ~/.gmail-mcp/. Skipping Gmail channel. Run /add-gmail to set up.', ); + return; } const keys = JSON.parse(fs.readFileSync(keysPath, 'utf-8')); @@ -86,17 +88,23 @@ export class GmailChannel implements Channel { this.userEmail = profile.data.emailAddress || ''; logger.info({ email: this.userEmail }, 'Gmail channel connected'); - // Start polling - this.pollTimer = setInterval( - () => - this.pollForMessages().catch((err) => - logger.error({ err }, 'Gmail poll error'), - ), - this.pollIntervalMs, - ); + // Start polling with error backoff + const schedulePoll = () => { + const backoffMs = this.consecutiveErrors > 0 + ? Math.min(this.pollIntervalMs * Math.pow(2, this.consecutiveErrors), 30 * 60 * 1000) + : this.pollIntervalMs; + this.pollTimer = setTimeout(() => { + this.pollForMessages() + .catch((err) => logger.error({ err }, 'Gmail poll error')) + .finally(() => { + if (this.gmail) schedulePoll(); + }); + }, backoffMs); + }; // Initial poll await this.pollForMessages(); + schedulePoll(); } async sendMessage(jid: string, text: string): Promise { @@ -158,7 +166,7 @@ export class GmailChannel implements Channel { async disconnect(): Promise { if (this.pollTimer) { - clearInterval(this.pollTimer); + clearTimeout(this.pollTimer); this.pollTimer = null; } this.gmail = null; @@ -197,8 +205,12 @@ export class GmailChannel implements Channel { const ids = [...this.processedIds]; this.processedIds = new Set(ids.slice(ids.length - 2500)); } + + this.consecutiveErrors = 0; } catch (err) { - logger.error({ err }, 'Gmail poll failed'); + this.consecutiveErrors++; + const backoffMs = Math.min(this.pollIntervalMs * Math.pow(2, this.consecutiveErrors), 30 * 60 * 1000); + logger.error({ err, consecutiveErrors: this.consecutiveErrors, nextPollMs: backoffMs }, 'Gmail poll failed'); } } diff --git a/.claude/skills/add-gmail/modify/src/index.ts b/.claude/skills/add-gmail/modify/src/index.ts index 1b04695..be26a17 100644 --- a/.claude/skills/add-gmail/modify/src/index.ts +++ b/.claude/skills/add-gmail/modify/src/index.ts @@ -452,7 +452,11 @@ async function main(): Promise { const gmail = new GmailChannel(channelOpts); channels.push(gmail); - await gmail.connect(); + try { + await gmail.connect(); + } catch (err) { + logger.warn({ err }, 'Gmail channel failed to connect, continuing without it'); + } // Start subsystems (independently of connection handler) startSchedulerLoop({