Skip to content

Add processed_mentions tracking to prevent duplicate replies and improve fallback message - #110

Open
groupthinking wants to merge 1 commit into
mainfrom
fix-spam
Open

Add processed_mentions tracking to prevent duplicate replies and improve fallback message#110
groupthinking wants to merge 1 commit into
mainfrom
fix-spam

Conversation

@groupthinking

Copy link
Copy Markdown
Owner

No description provided.

Copilot AI review requested due to automatic review settings July 24, 2026 22:43
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary

  • Added persistent processed_mentions tracking to prevent duplicate replies across restarts.
  • Added fallback apology text when Grok response generation fails, allowing tweet creation to continue.
  • Updated timeline API defaults from port 8080 to 8000.
  • Removed an obsolete comment near the X 402 Payment Required backoff logic.

Walkthrough

listener.py now persists processed X mention IDs, skips duplicates across restarts, falls back to an apology when Grok fails, records successful replies, and defaults timeline API calls to port 8000.

Changes

Mention processing

Layer / File(s) Summary
Processed mention storage
listener.py
Adds configurable persistent storage for processed mention IDs, including loading and append helpers.
Mention polling and reply completion
listener.py
Loads processed IDs, skips duplicates, uses an apology fallback for Grok failures, and persists IDs after successful replies.
Timeline API defaults
listener.py
Changes timeline publishing and agent registration defaults to port 8000; 402 handling logic is otherwise unchanged.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant X_API
  participant listener.py
  participant Grok
  participant processed_mentions_file
  X_API->>listener.py: provide mentions
  listener.py->>listener.py: skip previously processed IDs
  listener.py->>Grok: request reply
  Grok-->>listener.py: reply or failure
  listener.py->>X_API: create reply tweet
  listener.py->>processed_mentions_file: persist mention ID
Loading

Suggested reviewers: copilot

Poem

Mentions knock at the polling door,
Old IDs get ignored once more.
Grok stumbles; apologies fly,
New replies leave their IDs nearby.
Port eight hundred waits in line—
The listener grumbles, “Fine.”

🚥 Pre-merge checks | ✅ 4 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Enforce Copilot Verification ⚠️ Warning PR #110 only has a Copilot COMMENTED review; no APPROVED Copilot review exists. Get an explicit APPROVED review from copilot-pull-request-reviewer[bot]; human approvals don't count.
Require Ai Unit Tests ⚠️ Warning PR metadata shows no copilot-rabbit label, and the diff only changes listener.py—no unit tests were added. Add the copilot-rabbit label and commit AI-generated unit tests alongside the listener.py changes.
Description check ❓ Inconclusive No pull request description was provided, so there is nothing meaningful to validate. Add a brief description of the behavior change, especially the dedup persistence and fallback handling.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title matches the main change: deduping processed mentions and changing the Grok fallback path.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-spam
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix-spam

Warning

Review ran into problems

🔥 Problems

These MCP integrations need to be re-authenticated in the Integrations settings: Sentry


Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds persistent tracking of processed X mention IDs in the Python listener to prevent duplicate replies across polling cycles/restarts, and updates the fallback reply message when Grok generation fails.

Changes:

  • Persist processed mention IDs to a local file and skip already-processed mentions.
  • Update the Grok error fallback reply text.
  • Change the listener’s default Timeline API URL (currently to port 8000).
Comments suppressed due to low confidence (1)

listener.py:100

  • The default TIMELINE_API_URL port was changed to 8000 here, but the rest of the repo (env.example, docker-compose, mcp_dispatcher.py) still defaults the timeline server to 8080. If TIMELINE_API_URL isn’t set, agent registration will POST to the wrong local endpoint.
    timeline_url = os.getenv("TIMELINE_API_URL", "http://127.0.0.1:8000")

Comment thread listener.py

def push_timeline_card(title: str, body: str, metadata: dict) -> None:
timeline_url = os.getenv("TIMELINE_API_URL", "http://127.0.0.1:8080")
timeline_url = os.getenv("TIMELINE_API_URL", "http://127.0.0.1:8000")
Comment thread listener.py
Comment on lines +156 to +160
mention_id_str = str(mention.id)
if mention_id_str in processed_mentions:
print(f"Skipping already processed mention {mention.id}", flush=True)
continue

Comment thread listener.py
Comment on lines 176 to 184
try:
client.create_tweet(
text=grok_reply[:280],
in_reply_to_tweet_id=mention.id,
)
processed_mentions.add(mention_id_str)
save_processed_mention(mention_id_str)
except Exception as exc:
print(f"Error replying to mention {mention.id}: {exc}", flush=True)
Comment thread listener.py
Comment on lines +38 to +42
def load_processed_mentions() -> set[str]:
if not PROCESSED_MENTIONS_PATH.exists():
return set()
with PROCESSED_MENTIONS_PATH.open("r", encoding="utf-8") as f:
return {line.strip() for line in f if line.strip()}
Comment thread listener.py
POLL_SECONDS = int(os.getenv("POLL_INTERVAL_SECONDS", "60"))
PAYMENT_REQUIRED_BACKOFF_SECONDS = int(os.getenv("X_PAYMENT_REQUIRED_BACKOFF_SECONDS", "900"))

PROCESSED_MENTIONS_PATH = Path(os.getenv("XMCP_PROCESSED_MENTIONS_PATH", "~/.xmcp/processed_mentions.txt")).expanduser()

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@listener.py`:
- Around line 45-48: Replace the unbounded append-only storage in
save_processed_mention with a bounded durable store, or add safe
compaction/retention tied to the listener’s replay window. Ensure compaction
preserves every mention ID still needed to prevent replay while removing older
entries, and keep startup loading bounded accordingly.
- Around line 171-174: Update the get_grok_reply flow in the mention handler so
failure-like return values such as “Missing XAI_API_KEY.” and “Thinking...” are
detected before create_tweet(). Route them through the existing apology fallback
and ensure they are not published or recorded as successful replies; preserve
the current exception handling behavior for raised errors.
- Around line 38-42: Update load_processed_mentions() to handle filesystem read
errors without silently returning an empty set: retry transient failures or
propagate an explicit health failure. Adjust main() so failures from
load_processed_mentions() are handled within the daemon polling lifecycle,
preventing the listener thread from terminating while preserving the fail-closed
behavior that avoids duplicate replies.
- Around line 176-182: The mention handler around create_tweet and
save_processed_mention must distinguish successful tweet delivery from durable
state persistence. Do not let save_processed_mention failures enter the “Error
replying” path or treat the tweet as unsent; make persistence failures retry or
fail closed, and only update processed_mentions consistently with a successful
durable commit to prevent replay after restart.
- Line 86: Align the TIMELINE_API_URL fallback consistently across listener.py,
env.example, and agents/base.py, using the same endpoint default everywhere
(preferably port 8080 to match the existing configuration). Update the listener
registration and timeline-card request paths and the documented example
together, or make the variable mandatory in all locations.
- Around line 156-159: Update the mention-processing flow around the
processed_mentions check so the persisted start_time/cursor advances
monotonically to the newest processed mention, rather than being overwritten for
each newest-first item. Track the maximum tweet ID or timestamp across handled
mentions and persist that value only after processing the batch, while
preserving the skip behavior for already processed mentions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4e25f4c6-1faa-49f0-b6cb-fd85c8c5c777

📥 Commits

Reviewing files that changed from the base of the PR and between 8005bb6 and feca528.

📒 Files selected for processing (1)
  • listener.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • groupthinking/uvai-skills (manual)
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: copilot-pull-request-reviewer
🧰 Additional context used
🔍 Remote MCP GitHub Copilot

Additional PR context

  • PR #110 is open, has 1 commit, 1 changed file (listener.py), with +26/-4 lines; mergeable state is dirty. An auto-generated CodeRabbit comment says review is still in progress.
  • main’s listener.py already persists last_seen and processes mentions oldest-first; this PR adds a separate persisted processed_mentions set via XMCP_PROCESSED_MENTIONS_PATH, loads it at startup, skips already-processed mention IDs, and saves IDs after a successful reply.
  • The Grok-failure fallback text changes from Processing your tag... (error generating full response) to Sorry, I'm having trouble processing that. Try again or DM me.
  • The timeline API default in listener.py changes to http://127.0.0.1:8000, but env.example and agents/base.py still default TIMELINE_API_URL to http://127.0.0.1:8080; env.example also sets MCP_PORT=8000 and MCP_SERVER_URL=http://127.0.0.1:8000/mcp.
🔇 Additional comments (1)
listener.py (1)

19-19: LGTM!

Also applies to: 129-130, 140-141

Comment thread listener.py
Comment on lines +38 to +42
def load_processed_mentions() -> set[str]:
if not PROCESSED_MENTIONS_PATH.exists():
return set()
with PROCESSED_MENTIONS_PATH.open("r", encoding="utf-8") as f:
return {line.strip() for line in f if line.strip()}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not let a broken state file kill the listener.

load_processed_mentions() lets filesystem errors escape, and main() calls it outside the polling error handler. Because main.py:49-58 runs this in a daemon thread, an unreadable or temporarily unavailable file terminates mention processing. Retry or fail closed with an explicit health failure; never silently use an empty set, since that re-enables duplicate replies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@listener.py` around lines 38 - 42, Update load_processed_mentions() to handle
filesystem read errors without silently returning an empty set: retry transient
failures or propagate an explicit health failure. Adjust main() so failures from
load_processed_mentions() are handled within the daemon polling lifecycle,
preventing the listener thread from terminating while preserving the fail-closed
behavior that avoids duplicate replies.

Comment thread listener.py
Comment on lines +45 to +48
def save_processed_mention(mention_id: str) -> None:
PROCESSED_MENTIONS_PATH.parent.mkdir(parents=True, exist_ok=True)
with PROCESSED_MENTIONS_PATH.open("a", encoding="utf-8") as f:
f.write(f"{mention_id}\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

This processed-mention ledger grows forever.

Every successful mention appends another line, while startup loads the entire file into memory. Long-running usage will cause unbounded disk growth and increasingly expensive restarts. Use a bounded durable store or implement safe compaction/retention based on the replay window.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@listener.py` around lines 45 - 48, Replace the unbounded append-only storage
in save_processed_mention with a bounded durable store, or add safe
compaction/retention tied to the listener’s replay window. Ensure compaction
preserves every mention ID still needed to prevent replay while removing older
entries, and keep startup loading bounded accordingly.

Comment thread listener.py

def push_timeline_card(title: str, body: str, metadata: dict) -> None:
timeline_url = os.getenv("TIMELINE_API_URL", "http://127.0.0.1:8080")
timeline_url = os.getenv("TIMELINE_API_URL", "http://127.0.0.1:8000")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Align the timeline endpoint default everywhere.

listener.py now defaults to port 8000, while env.example and agents/base.py still default 8080. With no explicit TIMELINE_API_URL, registration and timeline-card requests hit the wrong service. Update all defaults together or require the variable explicitly.

Also applies to: 100-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@listener.py` at line 86, Align the TIMELINE_API_URL fallback consistently
across listener.py, env.example, and agents/base.py, using the same endpoint
default everywhere (preferably port 8080 to match the existing configuration).
Update the listener registration and timeline-card request paths and the
documented example together, or make the variable mandatory in all locations.

Comment thread listener.py
Comment on lines +156 to +159
mention_id_str = str(mention.id)
if mention_id_str in processed_mentions:
print(f"Skipping already processed mention {mention.id}", flush=True)
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
rg -n 'get_users_mentions|start_time|since_id|last_seen|processed_mentions' listener.py

Repository: groupthinking/MyXstack

Length of output: 1107


🏁 Script executed:

#!/bin/bash
set -eu

sed -n '110,215p' listener.py | cat -n

Repository: groupthinking/MyXstack

Length of output: 4377


🌐 Web query:

Tweepy get_users_mentions start_time inclusive second granularity order returned mentions newest first docs

💡 Result:

In Tweepy, the get_users_mentions method uses the Twitter API v2, which processes time parameters with second granularity [1][2]. Regarding your specific questions: 1. Inclusive Granularity: The start_time and end_time parameters are inclusive [1][2]. For example, a timestamp set to 12:00:01 includes the first second of that minute [1][2]. 2. Order of Returns: The endpoint returns mentions in reverse chronological order (newest first) by default. 3. Documentation: These details are documented in the Tweepy official documentation for Client.get_users_mentions [1][3]. Note that this endpoint is subject to Twitter API v2 limitations, such as restricted history access (typically up to the last 800 mentions) [1][4]. If you need access to older data, you may need to utilize different API endpoints depending on your access level [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

sed -n '1,110p' listener.py | cat -n

Repository: groupthinking/MyXstack

Length of output: 4603


Checkpoint is moving the wrong way

start_time gets overwritten for every mention, and Tweepy returns mentions newest-first. That leaves the next poll anchored to the oldest item already handled, so you keep re-fetching the same mentions. Persist a monotonic cursor (newest processed tweet ID/timestamp) instead of advancing per item.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@listener.py` around lines 156 - 159, Update the mention-processing flow
around the processed_mentions check so the persisted start_time/cursor advances
monotonically to the newest processed mention, rather than being overwritten for
each newest-first item. Track the maximum tweet ID or timestamp across handled
mentions and persist that value only after processing the batch, while
preserving the skip behavior for already processed mentions.

Comment thread listener.py
Comment on lines 171 to +174
grok_reply = get_grok_reply(prompt)
except Exception as exc:
print(f"Error getting Grok reply for mention {mention.id}: {exc}", flush=True)
grok_reply = "Processing your tag... (error generating full response)"
grok_reply = "Sorry, I'm having trouble processing that. Try again or DM me."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Route all failed Grok results through the apology fallback.

get_grok_reply() returns "Missing XAI_API_KEY." and "Thinking..." for failure-like paths instead of raising. This handler therefore publishes and records those placeholders as successful replies. Return a typed success/error result or raise for these cases before calling create_tweet().

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 172-172: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@listener.py` around lines 171 - 174, Update the get_grok_reply flow in the
mention handler so failure-like return values such as “Missing XAI_API_KEY.” and
“Thinking...” are detected before create_tweet(). Route them through the
existing apology fallback and ensure they are not published or recorded as
successful replies; preserve the current exception handling behavior for raised
errors.

Comment thread listener.py
Comment on lines 176 to +182
try:
client.create_tweet(
text=grok_reply[:280],
in_reply_to_tweet_id=mention.id,
)
processed_mentions.add(mention_id_str)
save_processed_mention(mention_id_str)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not treat state-persistence failure as tweet failure.

If create_tweet() succeeds but save_processed_mention() fails, this block logs “Error replying” after already sending the reply. The ID is also added to memory before the disk write, so only the current process suppresses a duplicate; after restart, the reply is sent again. Separate the external write from the durable state commit and retry/fail closed on persistence errors instead of blindly replaying a non-idempotent tweet.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@listener.py` around lines 176 - 182, The mention handler around create_tweet
and save_processed_mention must distinguish successful tweet delivery from
durable state persistence. Do not let save_processed_mention failures enter the
“Error replying” path or treat the tweet as unsent; make persistence failures
retry or fail closed, and only update processed_mentions consistently with a
successful durable commit to prevent replay after restart.

@groupthinking

Copy link
Copy Markdown
Owner Author

Do not resolve the conflicts on this PR. Re-cut the branch from current main instead.

I audited all five open PRs (#106#110) against the GitHub compare API. Every one of them is a long-stale branch, not a genuine content conflict:

PR Head branch Ahead Behind Merge base
#106 claude/myxstack-agent-merge-myaock 12 33 2026-04-20
#107 copilot/add-workflow-files 5 103 2026-01-28
#108 copilot/clear-pending-prs 4 107 2026-01-28
#109 copilot/clear-pending-prs-again 4 117 2026-01-27
#110 fix-spam 1 53 2026-02-12

All five report status=diverged.

Why the diffs look absurd

#109 is titled "fix: replace invalid CODEOWNERS entries with @groupthinking" but reports +30,880 / −216 across 38 files, with essentially every file showing +N / −0 — including openapi.json (+23,095), package-lock.json (+1,187), and MOLT_STRATEGIC_ANALYSIS.md (+1,075).

That is not what the PR intends to change. The branch was cut on 2026-01-27 and opened as a PR on 2026-07-24 — a six-month gap during which main advanced 117 commits. The diff is being computed against that January merge base, so it renders the branch's stale snapshot of the tree as bulk additions.

The conflicts are therefore real, but they are drift, not disagreement. Resolving them by hand means manually reconciling six months of divergence across 38 files to land what should be a handful of line edits — and every manual resolution is an opportunity to silently revert work that landed on main in the interim.

Resolution

For each PR, the actual intent is small and is captured in only 1–12 commits. Re-apply that intent on top of current main:

git fetch origin
git checkout -b <name>-rebased origin/main
git cherry-pick <the 1-12 real commits>   # or simply re-make the edit by hand

Then open a replacement PR and close the stale one. For #109 specifically, the intended change is a CODEOWNERS edit — that is a few lines, and re-making it by hand against current main is strictly faster and safer than reconciling a 30k-line diff.

Recommended disposition:

Root cause to fix going forward: these five PRs were all opened within a 112-second window (22:41:30 → 22:43:22 on 2026-07-24) from branches that were months old. Whatever automation opened them did not rebase first. Adding a staleness check — refuse to open a PR whose branch is more than N commits behind its base — would prevent this class of PR entirely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants