diff --git a/package-lock.json b/package-lock.json index 4025653..aa0b39b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@fullstackhouse/agentloop", - "version": "0.7.1", + "version": "0.7.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@fullstackhouse/agentloop", - "version": "0.7.1", + "version": "0.7.3", "license": "MIT", "dependencies": { "dotenv": "^16.4.7", diff --git a/package.json b/package.json index 9908475..9c1b081 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fullstackhouse/agentloop", - "version": "0.7.2", + "version": "0.7.3", "description": "AI agent that monitors chat platforms and responds using Claude Code", "repository": { "type": "git", diff --git a/src/cli.ts b/src/cli.ts index 37fd5f3..b8e2bd1 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -110,7 +110,15 @@ async function serve(options: CliOptions): Promise { } const slackApi = new SlackApi(xoxc, xoxd); - const adapter = new SlackAdapter(agent, slackApi, slackChannels, slackChannelBlacklist); + const adapter = new SlackAdapter( + agent, + slackApi, + slackChannels, + slackChannelBlacklist, + config.slackUsers, + 10_000, // pollIntervalMs + config.maxRetries, + ); await adapter.start(); adapters.push(adapter); } diff --git a/src/config.ts b/src/config.ts index 524893f..5b16317 100644 --- a/src/config.ts +++ b/src/config.ts @@ -37,6 +37,7 @@ export interface AppConfig { slackChannels?: string[]; slackChannelBlacklist?: string[]; slackUsers?: string[]; + maxRetries?: number; // Max retries before giving up on a message (default: 3) workspaceDir?: string; // Working directory for Claude Code subprocess mcpServers?: Record; } diff --git a/src/platforms/slack.ts b/src/platforms/slack.ts index 7f743b5..24a98d2 100644 --- a/src/platforms/slack.ts +++ b/src/platforms/slack.ts @@ -14,6 +14,8 @@ export class SlackAdapter { private threadSessions = new Map(); /** Cache of user IDs to display names */ private userNames = new Map(); + /** Tracks retry attempts per message */ + private retryCount = new Map(); constructor( private agent: Agent, @@ -22,6 +24,7 @@ export class SlackAdapter { private channelBlacklist?: string[], private users?: string[], private pollIntervalMs = 10_000, + private maxRetries = 3, ) {} async start(): Promise { @@ -262,11 +265,25 @@ ${cleanText} await this.slackApi.reactionsRemove(channel, msg.ts, 'eyes').catch(this.ignoreSlackError('no_reaction')); await this.slackApi.reactionsAdd(channel, msg.ts, 'white_check_mark').catch(this.ignoreSlackError('already_reacted')); + // Clear retry counter on success + this.retryCount.delete(key); + const elapsed = Date.now() - startTime; console.log(`[slack] ${key}: Completed successfully in ${elapsed}ms`); } catch (e) { const elapsed = Date.now() - startTime; - console.error(`[slack] ${key}: Failed after ${elapsed}ms:`, e); + const attempt = (this.retryCount.get(key) || 0) + 1; + this.retryCount.set(key, attempt); + + if (attempt < this.maxRetries) { + console.error(`[slack] ${key}: Failed after ${elapsed}ms (attempt ${attempt}/${this.maxRetries}), will retry:`, e); + // Don't add ✅, let next poll retry + return; + } + + console.error(`[slack] ${key}: Failed after ${elapsed}ms (attempt ${attempt}/${this.maxRetries}, giving up):`, e); + this.retryCount.delete(key); + try { const threadTs = msg.thread_ts || msg.ts; console.log(`[slack] ${key}: Notifying user of error`); @@ -274,6 +291,8 @@ ${cleanText} await this.slackApi.reactionsRemove(channel, msg.ts, 'eyes').catch((e) => { console.warn(`[slack] ${key}: Failed to remove eyes reaction:`, e); }); + // Add ✅ after max retries to prevent endless retry loop + await this.slackApi.reactionsAdd(channel, msg.ts, 'white_check_mark').catch(this.ignoreSlackError('already_reacted')); console.log(`[slack] ${key}: Error notification sent`); } catch (e) { console.warn(`[slack] ${key}: Failed to notify user of error:`, e);