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 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-02-25 00:23:11 +02:00
parent 695ff08f4c
commit 8e164f608c
2 changed files with 30 additions and 14 deletions

View File

@@ -34,9 +34,10 @@ export class GmailChannel implements Channel {
private gmail: gmail_v1.Gmail | null = null; private gmail: gmail_v1.Gmail | null = null;
private opts: GmailChannelOpts; private opts: GmailChannelOpts;
private pollIntervalMs: number; private pollIntervalMs: number;
private pollTimer: ReturnType<typeof setInterval> | null = null; private pollTimer: ReturnType<typeof setTimeout> | null = null;
private processedIds = new Set<string>(); private processedIds = new Set<string>();
private threadMeta = new Map<string, ThreadMeta>(); private threadMeta = new Map<string, ThreadMeta>();
private consecutiveErrors = 0;
private userEmail = ''; private userEmail = '';
constructor(opts: GmailChannelOpts, pollIntervalMs = 60000) { constructor(opts: GmailChannelOpts, pollIntervalMs = 60000) {
@@ -50,9 +51,10 @@ export class GmailChannel implements Channel {
const tokensPath = path.join(credDir, 'credentials.json'); const tokensPath = path.join(credDir, 'credentials.json');
if (!fs.existsSync(keysPath) || !fs.existsSync(tokensPath)) { if (!fs.existsSync(keysPath) || !fs.existsSync(tokensPath)) {
throw new Error( logger.warn(
'Gmail credentials not found in ~/.gmail-mcp/. Run Gmail OAuth setup first.', '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')); const keys = JSON.parse(fs.readFileSync(keysPath, 'utf-8'));
@@ -86,17 +88,23 @@ export class GmailChannel implements Channel {
this.userEmail = profile.data.emailAddress || ''; this.userEmail = profile.data.emailAddress || '';
logger.info({ email: this.userEmail }, 'Gmail channel connected'); logger.info({ email: this.userEmail }, 'Gmail channel connected');
// Start polling // Start polling with error backoff
this.pollTimer = setInterval( const schedulePoll = () => {
() => const backoffMs = this.consecutiveErrors > 0
this.pollForMessages().catch((err) => ? Math.min(this.pollIntervalMs * Math.pow(2, this.consecutiveErrors), 30 * 60 * 1000)
logger.error({ err }, 'Gmail poll error'), : this.pollIntervalMs;
), this.pollTimer = setTimeout(() => {
this.pollIntervalMs, this.pollForMessages()
); .catch((err) => logger.error({ err }, 'Gmail poll error'))
.finally(() => {
if (this.gmail) schedulePoll();
});
}, backoffMs);
};
// Initial poll // Initial poll
await this.pollForMessages(); await this.pollForMessages();
schedulePoll();
} }
async sendMessage(jid: string, text: string): Promise<void> { async sendMessage(jid: string, text: string): Promise<void> {
@@ -158,7 +166,7 @@ export class GmailChannel implements Channel {
async disconnect(): Promise<void> { async disconnect(): Promise<void> {
if (this.pollTimer) { if (this.pollTimer) {
clearInterval(this.pollTimer); clearTimeout(this.pollTimer);
this.pollTimer = null; this.pollTimer = null;
} }
this.gmail = null; this.gmail = null;
@@ -197,8 +205,12 @@ export class GmailChannel implements Channel {
const ids = [...this.processedIds]; const ids = [...this.processedIds];
this.processedIds = new Set(ids.slice(ids.length - 2500)); this.processedIds = new Set(ids.slice(ids.length - 2500));
} }
this.consecutiveErrors = 0;
} catch (err) { } 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');
} }
} }

View File

@@ -452,7 +452,11 @@ async function main(): Promise<void> {
const gmail = new GmailChannel(channelOpts); const gmail = new GmailChannel(channelOpts);
channels.push(gmail); 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) // Start subsystems (independently of connection handler)
startSchedulerLoop({ startSchedulerLoop({