From be0a4ae2ad077093882c9ab1591bd04147e350b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:50:38 +0000 Subject: [PATCH 1/4] Initial plan From a3d5392f033758ac453bbfe2df4736220f55dd90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:53:12 +0000 Subject: [PATCH 2/4] Initial plan for consolidating PR review feedback Co-authored-by: groupthinking <154503486+groupthinking@users.noreply.github.com> --- package-lock.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package-lock.json b/package-lock.json index 9b3d662..790aab4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -395,6 +395,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -1158,6 +1159,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 76b058178d0921dbb0c215f5184a1f81b0f95c62 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:58:31 +0000 Subject: [PATCH 3/4] fix: address review feedback from PRs #6, #26 - reply targeting, type safety, Array.isArray guards, Set pruning Co-authored-by: groupthinking <154503486+groupthinking@users.noreply.github.com> --- src/examples.ts | 4 ++-- src/services/agent.ts | 20 ++++++++++++++----- src/services/grok.ts | 19 +++++++++--------- src/services/xapi.ts | 46 ++++++++++++++++++++++++++++++------------- 4 files changed, 59 insertions(+), 30 deletions(-) diff --git a/src/examples.ts b/src/examples.ts index aaf1598..3da6efe 100644 --- a/src/examples.ts +++ b/src/examples.ts @@ -33,7 +33,7 @@ async function example1_fetchAndAnalyzeMention() { console.log(`\nThread has ${thread.replies.length + 1} posts`); // Analyze with Grok - const analysis = await grok.analyzeAndDecide(mention.post.text, thread); + const analysis = await grok.analyzeAndDecide(mention.post.text, thread, mention.post.id); console.log(`\nGrok's Decision:`); console.log(` Action: ${analysis.action.type}`); @@ -139,7 +139,7 @@ async function example5_batchProcessMentions() { const thread = await xClient.fetchThread(conversationId); if (thread) { - const analysis = await grok.analyzeAndDecide(mention.post.text, thread); + const analysis = await grok.analyzeAndDecide(mention.post.text, thread, mention.post.id); console.log(` → Action: ${analysis.action.type} (${(analysis.confidence * 100).toFixed(0)}% confidence)`); // In a real scenario, you might execute the action here diff --git a/src/services/agent.ts b/src/services/agent.ts index ab40033..c12b639 100644 --- a/src/services/agent.ts +++ b/src/services/agent.ts @@ -7,6 +7,8 @@ import { GrokService } from '../services/grok.js'; import { AgentConfig, Mention, AgentAction } from '../types/index.js'; export class AutonomousAgent { + private static readonly MAX_PROCESSED_MENTIONS = 1000; + private xClient: XAPIClient; private grokService: GrokService; private config: AgentConfig; @@ -90,10 +92,17 @@ export class AutonomousAgent { console.log(`\nšŸ“¬ [${new Date().toLocaleTimeString()}] Found ${newMentions.length} new mention(s)!\n`); - // Process each mention - for (const mention of newMentions) { - await this.processMention(mention); - this.processedMentions.add(mention.post.id); + // Process each mention (oldest first for chronological Set insertion order) + for (let i = newMentions.length - 1; i >= 0; i--) { + await this.processMention(newMentions[i]); + this.processedMentions.add(newMentions[i].post.id); + } + + // Prune oldest entries if Set grows too large (keeping newest MAX_PROCESSED_MENTIONS) + if (this.processedMentions.size > AutonomousAgent.MAX_PROCESSED_MENTIONS) { + const entriesToDelete = Array.from(this.processedMentions.values()) + .slice(0, this.processedMentions.size - AutonomousAgent.MAX_PROCESSED_MENTIONS); + entriesToDelete.forEach(id => this.processedMentions.delete(id)); } } catch (error) { console.error('āŒ Error in processing loop:', error); @@ -129,7 +138,8 @@ export class AutonomousAgent { console.log('\nšŸ¤– Analyzing with Grok AI...'); const analysis = await this.grokService.analyzeAndDecide( mention.post.text, - thread + thread, + mention.post.id ); console.log(` Action: ${analysis.action.type.toUpperCase()}`); diff --git a/src/services/grok.ts b/src/services/grok.ts index 0fd6b2c..cdbf0e0 100644 --- a/src/services/grok.ts +++ b/src/services/grok.ts @@ -21,11 +21,12 @@ export class GrokService { * Analyze a mention and thread context to determine appropriate action * @param mention - The text content of the mention to analyze * @param thread - The thread context including root post and replies + * @param mentionPostId - The ID of the specific mention post to target in replies * @returns Analysis with recommended action */ - async analyzeAndDecide(mention: string, thread: XThread): Promise { + async analyzeAndDecide(mention: string, thread: XThread, mentionPostId: string): Promise { if (this.simulationMode) { - return this.simulateAnalysis(mention, thread); + return this.simulateAnalysis(mention, thread, mentionPostId); } try { @@ -58,15 +59,15 @@ export class GrokService { throw new Error(`Grok API error: ${response.status}`); } - const data: any = await response.json(); + const data = await response.json() as { choices: Array<{ message?: { content?: string } }> }; const analysisText = data.choices[0]?.message?.content || ''; - // Use the root post ID from the thread, not the mention text - return this.parseGrokResponse(analysisText, thread.root_post.id); + // Use the specific mention post ID for targeted replies + return this.parseGrokResponse(analysisText, mentionPostId); } catch (error) { console.error('Error calling Grok API:', error); // Fallback to simulation - return this.simulateAnalysis(mention, thread); + return this.simulateAnalysis(mention, thread, mentionPostId); } } @@ -145,7 +146,7 @@ export class GrokService { /** * Simulate Grok analysis for testing */ - private simulateAnalysis(mention: string, thread: XThread): GrokAnalysis { + private simulateAnalysis(mention: string, thread: XThread, mentionPostId: string): GrokAnalysis { console.log('šŸ¤– Simulated Grok Analysis:'); console.log(` Analyzing: "${mention}"`); @@ -159,7 +160,7 @@ export class GrokService { const analysis: GrokAnalysis = { action: { type: 'reply', - target_post_id: thread.root_post.id, + target_post_id: mentionPostId, content: 'Thanks for reaching out! I\'ve analyzed your question and here\'s my insight: Based on the context, I\'d recommend exploring this topic further. Let me know if you need more specific information!', reasoning: 'Detected a question, providing helpful response', }, @@ -174,7 +175,7 @@ export class GrokService { const analysis: GrokAnalysis = { action: { type: 'analyze', - target_post_id: thread.root_post.id, + target_post_id: mentionPostId, reasoning: 'No clear action needed, just acknowledgment', }, confidence: 0.7, diff --git a/src/services/xapi.ts b/src/services/xapi.ts index 1f62056..e37c729 100644 --- a/src/services/xapi.ts +++ b/src/services/xapi.ts @@ -5,6 +5,8 @@ import { XPost, XThread, Mention, XAPIConfig } from '../types/index.js'; export class XAPIClient { + private static readonly DEFAULT_MAX_MENTIONS = 10; + private config: XAPIConfig; private lastMentionId: string | null = null; private simulationMode: boolean = false; @@ -44,8 +46,16 @@ export class XAPIClient { throw new Error('Failed to get user ID from response'); } + const params = new URLSearchParams({ + max_results: XAPIClient.DEFAULT_MAX_MENTIONS.toString(), + expansions: 'author_id', + 'tweet.fields': 'created_at,conversation_id,in_reply_to_user_id,referenced_tweets', + }); + if (this.lastMentionId) { + params.set('since_id', this.lastMentionId); + } const mentionsResponse = await this.makeXAPIRequest( - `https://api.twitter.com/2/users/${userId}/mentions?max_results=10&expansions=author_id&tweet.fields=created_at,conversation_id,in_reply_to_user_id,referenced_tweets`, + `https://api.twitter.com/2/users/${userId}/mentions?${params.toString()}`, 'GET' ); @@ -77,7 +87,12 @@ export class XAPIClient { 'GET' ); - return this.parseThread(response.data || []); + if (!Array.isArray(response.data)) { + console.warn('Unexpected response shape from X API (thread): data is not an array'); + return null; + } + + return this.parseThread(response.data); } catch (error) { console.error('Error fetching thread:', error); return null; @@ -127,7 +142,10 @@ export class XAPIClient { 'GET' ); - return (response.data || []).map((tweet: any) => this.parsePost(tweet)); + if (!Array.isArray(response.data)) { + return []; + } + return response.data.map((tweet: { [key: string]: unknown }) => this.parsePost(tweet)); } catch (error) { console.error('Error searching tweets:', error); return []; @@ -160,7 +178,7 @@ export class XAPIClient { return response.json(); } - private parseMentions(tweets: any[]): Mention[] { + private parseMentions(tweets: { created_at: string; [key: string]: unknown }[]): Mention[] { return tweets.map((tweet) => ({ post: this.parsePost(tweet), mentioned_at: new Date(tweet.created_at), @@ -168,20 +186,20 @@ export class XAPIClient { })); } - private parsePost(tweet: any): XPost { + private parsePost(tweet: { [key: string]: unknown }): XPost { return { - id: tweet.id, - text: tweet.text, - author_id: tweet.author_id, - author_username: tweet.username || 'unknown', - created_at: tweet.created_at, - conversation_id: tweet.conversation_id, - in_reply_to_user_id: tweet.in_reply_to_user_id, - referenced_tweets: tweet.referenced_tweets, + id: tweet.id as string, + text: tweet.text as string, + author_id: tweet.author_id as string, + author_username: (tweet.username as string) || 'unknown', + created_at: tweet.created_at as string, + conversation_id: tweet.conversation_id as string | undefined, + in_reply_to_user_id: tweet.in_reply_to_user_id as string | undefined, + referenced_tweets: tweet.referenced_tweets as Array<{ type: string; id: string }> | undefined, }; } - private parseThread(tweets: any[]): XThread | null { + private parseThread(tweets: { created_at: string; [key: string]: unknown }[]): XThread | null { if (tweets.length === 0) return null; const sorted = tweets.sort((a, b) => From 0a598d8f786fc7494118d3756deefffcb95c8a6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:59:24 +0000 Subject: [PATCH 4/4] refactor: use iterator-based Set pruning and clarify comment for reverse iteration Co-authored-by: groupthinking <154503486+groupthinking@users.noreply.github.com> --- src/services/agent.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/services/agent.ts b/src/services/agent.ts index c12b639..8021b98 100644 --- a/src/services/agent.ts +++ b/src/services/agent.ts @@ -92,17 +92,20 @@ export class AutonomousAgent { console.log(`\nšŸ“¬ [${new Date().toLocaleTimeString()}] Found ${newMentions.length} new mention(s)!\n`); - // Process each mention (oldest first for chronological Set insertion order) + // Process oldest first: X API returns newest-first, so iterate in reverse for (let i = newMentions.length - 1; i >= 0; i--) { await this.processMention(newMentions[i]); this.processedMentions.add(newMentions[i].post.id); } - // Prune oldest entries if Set grows too large (keeping newest MAX_PROCESSED_MENTIONS) + // Prune oldest entries if Set grows too large if (this.processedMentions.size > AutonomousAgent.MAX_PROCESSED_MENTIONS) { - const entriesToDelete = Array.from(this.processedMentions.values()) - .slice(0, this.processedMentions.size - AutonomousAgent.MAX_PROCESSED_MENTIONS); - entriesToDelete.forEach(id => this.processedMentions.delete(id)); + const iter = this.processedMentions.values(); + while (this.processedMentions.size > AutonomousAgent.MAX_PROCESSED_MENTIONS) { + const { value, done } = iter.next(); + if (done) break; + this.processedMentions.delete(value); + } } } catch (error) { console.error('āŒ Error in processing loop:', error);