Skip to content

Conversation

@anabelle
Copy link
Owner

@anabelle anabelle commented Oct 18, 2025

Description

This PR fixes the issue where the agent was replying multiple times to the same home feed posts, particularly posts with images. The second reply would come approximately 15 minutes after the first reply with a similar but different message.

Root Cause

The processHomeFeed function in plugin-nostr/lib/service.js lacked reply deduplication logic. Unlike mention handling which checks for existing replies before posting, home feed replies could be sent multiple times to the same post.

Solution

Added reply deduplication check in the home feed reply logic, similar to the existing deduplication used for mention handling:

  • Check for existing replies in recent memory (up to 100 messages) before posting
  • Skip posting if a reply to the same event already exists
  • Log when skipping due to deduplication

Changes

  • plugin-nostr/lib/service.js: Added deduplication check before postReply call in home feed processing

Testing

  • Existing test suite passes, ensuring no regressions
  • The deduplication logic follows the same pattern as existing code

Related Issue

Fixes #71: Bug: Duplicate replies to home feed posts

Impact

  • Prevents duplicate replies to home feed posts
  • Improves user experience by avoiding spam-like behavior
  • Reduces unnecessary API calls and processing

Summary by CodeRabbit

  • Bug Fixes

    • Implemented in-memory deduplication to prevent posting duplicate replies to the same post across home feed and discovery reply flows.
  • Tests

    • Added comprehensive test suite validating deduplication logic, covering scenarios with and without existing replies.

Copilot AI review requested due to automatic review settings October 18, 2025 14:35
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 18, 2025

Warning

Rate limit exceeded

@anabelle has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 13 minutes and 32 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 6851a86 and 3404a5e.

📒 Files selected for processing (1)
  • plugin-nostr/test/service.homeFeedDeduplication.test.js (1 hunks)

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Added in-memory deduplication checks to skip posting duplicate replies in both home-feed and discovery reply paths; also added tests verifying home-feed deduplication across repeated processing runs.

Changes

Cohort / File(s) Summary
Core dedupe updates
plugin-nostr/lib/service.js
Inserted in-flow in-memory deduplication: compute stable eventMemoryId, fetch recent memories via runtime.getMemories, detect prior replies by comparing inReplyTo to the eventMemoryId or original id, log and skip LLM/posting when duplicates found. Applied to home feed and discovery reply flows.
Home-feed dedupe tests
plugin-nostr/test/service.homeFeedDeduplication.test.js
New test suite that mocks runtime, memory, and service internals to assert: (a) first processHomeFeed posts a reply when no prior memory exists; (b) subsequent processHomeFeed skips posting when a prior reply memory is present.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Scheduler as processHomeFeed
    participant Service as NostrService
    participant Memory as Runtime Memory
    participant LLM as LLM / Text Gen
    participant Images as Image Processor
    participant Post as postReply

    Note over Scheduler,Post: Normal flow (no prior reply)
    Scheduler->>Service: process event
    Service->>Service: compute context/thread
    Service->>LLM: generate reply text
    LLM-->>Service: text
    Service->>Memory: createUniqueUuid(evt.id) & getMemories(...)
    Memory-->>Service: return recent memories (no match)
    Service->>Images: optional image processing
    Images-->>Service: images processed
    Service->>Post: postReply(text)
    Post-->>Service: posted ✓
    
    Note over Scheduler,Post: Dedupe flow (prior reply exists)
    Scheduler->>Service: process same event
    Service->>Service: compute context/thread
    Service->>LLM: generate reply text
    LLM-->>Service: text
    Service->>Memory: createUniqueUuid(evt.id) & getMemories(...)
    Memory-->>Service: return recent memories (match found)
    Service->>Service: log skip, do not call Images or Post
    Service-->>Scheduler: skip posting ✗
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐇 I peeked in the mem'ry, one hop, one glance,
No echoing replies, I leave none to chance.
If I've answered once, I softly decline,
One carrot, one note — polite and fine.
— your deduping rabbit 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes Check ❓ Inconclusive While the primary changes to home feed reply deduplication directly address issue #71, the PR also introduces deduplication logic to discovery replies, which extends beyond the explicit scope of the linked issue. Issue #71 specifically targets duplicate replies in home feed post processing, and the proposed fix is narrowly scoped to that function. The extension to discovery replies, though following the same pattern and potentially beneficial, was not explicitly required by or mentioned in the linked issue, making it unclear whether this expansion is intentional or was introduced alongside the primary fix without deliberate scope planning. To resolve this ambiguity, clarify whether the discovery reply deduplication changes are intentional scope enhancements or if they should be separated into a distinct follow-up PR. If this is a deliberate expansion to prevent the same duplication issue in a related code path, consider updating the PR description or issue to document this broader scope. If unintentional, consider reverting the discovery reply changes to focus this PR solely on issue #71's requirements.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Fix duplicate replies to home feed posts" directly reflects the primary objective of this PR as described in the linked issue #71. It clearly and concisely communicates that the fix addresses duplicate replies occurring in home feed processing, which is the core issue being resolved. The title is specific enough that reviewers scanning the history would immediately understand what problem was being solved without ambiguity or vague terminology.
Linked Issues Check ✅ Passed The PR implementation directly addresses all the coding requirements specified in issue #71. The changes add in-memory deduplication logic before posting replies in the home feed flow by computing an eventMemoryId, retrieving recent memories, and checking for existing replies before posting. When a duplicate is detected, the code logs the event and skips posting, exactly matching the proposed fix in the issue. The accompanying test suite validates that the deduplication logic works correctly by ensuring replies are not sent twice to the same post and are sent when no existing reply is found, confirming the implementation meets the stated objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Adds reply deduplication to home feed processing to prevent multiple replies to the same post and introduces tests verifying the behavior.

  • Implements a memory check before posting a reply in processHomeFeed
  • Adds unit tests covering reply when no prior reply exists and skip when a prior reply exists

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
plugin-nostr/lib/service.js Adds deduplication check in home feed reply path before calling postReply
plugin-nostr/test/service.homeFeedDeduplication.test.js Introduces tests to validate deduplication behavior for home feed replies

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Add reply deduplication logic in processHomeFeed function to prevent
the agent from replying multiple times to the same home feed post.

The fix checks for existing replies in memory before posting a new
reply, similar to the deduplication logic used for mention handling.

This resolves issue #71: Bug: Duplicate replies to home feed posts

- Added deduplication check before calling postReply in home feed reply logic
- Check looks for existing replies in recent memory (up to 100 messages)
- Uses the same pattern as mention handling for consistency
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
plugin-nostr/test/service.homeFeedDeduplication.test.js (2)

80-167: Refactor duplicated mock setup into shared helper.

Lines 85–104 and 140–159 repeat nearly identical mock assignments. Extract this common setup into a helper function to reduce duplication and improve maintainability.

Example refactor:

function setupHomeFeedMocks(service) {
  service._loadCurrentContacts = vi.fn(() => Promise.resolve(new Set(['test-pubkey'])));
  service._list = vi.fn(() => Promise.resolve([{
    id: 'test-event-id',
    pubkey: 'test-pubkey',
    content: 'Test post content',
    created_at: Math.floor(Date.now() / 1000),
    kind: 1
  }]));
  service._isQualityContent = vi.fn(() => true);
  service._analyzePostForInteraction = vi.fn(() => Promise.resolve(true));
  service._isUserMuted = vi.fn(() => Promise.resolve(false));
  service._getThreadContext = vi.fn(() => Promise.resolve([]));
  service._getConversationIdFromEvent = vi.fn(() => 'test-conv-id');
  service._ensureNostrContext = vi.fn(() => Promise.resolve({ roomId: 'test-room-id' }));
  service._shouldEngageWithThread = vi.fn(() => true);
  service.generateReplyTextLLM = vi.fn(() => Promise.resolve('Test reply text'));
  service.postReply = vi.fn(() => Promise.resolve(true));
  service.imageProcessingEnabled = false;
  service.createUniqueUuid = vi.fn((_, seed) => `uuid-${seed}`);
  service._chooseInteractionType = vi.fn(() => 'reply');
}

Then call setupHomeFeedMocks(service); in each test.


81-81: Remove console.log debugging statements.

Console.log statements at lines 81, 122, and 124 are debugging artifacts and should be removed before merging.

Apply this diff:

-      console.log('Starting test: should not reply twice');
       service = await NostrService.start(mockRuntime);
       
       // ... 
       
-      // First call to processHomeFeed - should reply
-      console.log('Calling processHomeFeed first time');
+      // First call to processHomeFeed - should reply
       await service.processHomeFeed();
-      console.log('processHomeFeed completed first time, postReply calls:', service.postReply.mock.calls.length);
       expect(service.postReply).toHaveBeenCalledTimes(1);

Also applies to: 122-122, 124-124

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f060b9 and b2fca2c.

📒 Files selected for processing (2)
  • plugin-nostr/lib/service.js (1 hunks)
  • plugin-nostr/test/service.homeFeedDeduplication.test.js (1 hunks)
🧰 Additional context used
🪛 GitHub Actions: Test plugin-nostr on PR
plugin-nostr/test/service.homeFeedDeduplication.test.js

[error] 125-125: AssertionError: expected postReply to have been called 1 times, but was called 0 times in 'should not reply twice to the same home feed post'.


[error] 166-166: AssertionError: expected postReply to have been called 1 times, but was called 0 times in 'should reply when no existing reply is found'.

🪛 Gitleaks (8.28.0)
plugin-nostr/test/service.homeFeedDeduplication.test.js

[high] 16-16: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (2)
plugin-nostr/lib/service.js (1)

5856-5893: Run dedup before invoking the LLM

The new dedup guard still calls generateReplyTextLLM on every pass before discovering that we already replied. In practice, this means the second (and third, …) cron runs will continue to hit the expensive model call on the same event even though we skip posting, defeating a big part of the “reduce unnecessary API calls/processing” goal. Please perform the memory lookup and early-return before the LLM invocation so repeated runs bail out without spending tokens.

plugin-nostr/test/service.homeFeedDeduplication.test.js (1)

16-16: Static analysis false positive: hardcoded key is a test fixture.

The static analysis tool flags line 16 as containing a sensitive API key. This is a test fixture (dummy private key for mocking), not a real secret. You can safely ignore this warning or add a gitleaks exception comment if needed.

@anabelle anabelle force-pushed the fix-duplicate-home-feed-replies branch from b2fca2c to 83f9792 Compare October 18, 2025 14:41
- Move deduplication check before LLM call to prevent unnecessary API usage
- Clean up test code by removing console.log statements and refactoring duplicated mocks
- Add helper function for test setup to improve maintainability
@anabelle anabelle requested a review from Copilot October 18, 2025 14:43
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

useModel: vi.fn(() => Promise.resolve({ text: 'Test reply' })),
createMemory: vi.fn(),
getMemoryById: vi.fn(),
getMemories: vi.fn(() => []),
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

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

[nitpick] Mock getMemories to return a promise consistently (it’s awaited in production code) to avoid accidental sync/async mismatches: getMemories: vi.fn(() => Promise.resolve([])).

Suggested change
getMemories: vi.fn(() => []),
getMemories: vi.fn(() => Promise.resolve([])),

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
plugin-nostr/test/service.homeFeedDeduplication.test.js (1)

110-134: Critical: Closure scope prevents deduplication test from exercising the dedupe path.

The getMemories mock (lines 110-122) captures callCount in its closure. Resetting callCount = 0 on line 129 doesn't affect the closure's captured variable—the second processHomeFeed() call continues incrementing from the closure's state, likely returning [] instead of the expected reply array. This prevents the deduplication logic from being tested.

Apply this diff to fix the mock sequencing:

-      // Mock getMemories to return existing reply on second call
-      let callCount = 0;
-      mockRuntime.getMemories = vi.fn(() => {
-        callCount++;
-        if (callCount === 1) {
-          // First call - no existing replies
-          return Promise.resolve([]);
-        } else {
-          // Second call - existing reply found
-          return Promise.resolve([{
-            content: { inReplyTo: 'uuid-test-event-id' }
-          }]);
-        }
-      });
+      // Mock getMemories to return no existing replies on first call
+      mockRuntime.getMemories = vi.fn()
+        .mockResolvedValueOnce([])  // First call - no existing replies
+        .mockResolvedValue([{       // Subsequent calls - existing reply found
+          content: { inReplyTo: 'uuid-test-event-id' }
+        }]);

       // First call to processHomeFeed - should reply
       await service.processHomeFeed();
       expect(service.postReply).toHaveBeenCalledTimes(1);

-      // Reset call count and mocks for second call
-      callCount = 0;
       service.postReply.mockClear();

       // Second call to processHomeFeed - should NOT reply due to deduplication
       await service.processHomeFeed();
       expect(service.postReply).not.toHaveBeenCalled();

Based on past review comments.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2fca2c and 6851a86.

📒 Files selected for processing (2)
  • plugin-nostr/lib/service.js (1 hunks)
  • plugin-nostr/test/service.homeFeedDeduplication.test.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
plugin-nostr/lib/service.js (3)
plugin-nostr/test-eliza-integration.js (1)
  • recent (49-49)
plugin-nostr/lib/context.js (3)
  • require (175-175)
  • roomId (6-6)
  • roomId (151-151)
plugin-nostr/lib/lnpixels-listener.js (7)
  • require (1-1)
  • require (13-13)
  • require (14-14)
  • require (88-88)
  • require (89-89)
  • roomId (17-17)
  • roomId (91-91)
🪛 GitHub Actions: Test plugin-nostr on PR
plugin-nostr/test/service.homeFeedDeduplication.test.js

[error] 1-1: Command: npm run test. Failure in Home Feed Deduplication tests: expected spy to be called 1 times, but got 0 times (should not reply twice to the same home feed post).


[error] 1-1: Command: npm run test. Failure in Home Feed Deduplication tests: expected spy to be called 1 times, but got 0 times (should reply when no existing reply is found).

🪛 Gitleaks (8.28.0)
plugin-nostr/test/service.homeFeedDeduplication.test.js

[high] 16-16: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (1)
plugin-nostr/test/service.homeFeedDeduplication.test.js (1)

16-16: Gitleaks warning is a false positive—test fixture properly documented.

The static analysis tool flags this as a potential secret exposure, but the inline comment clearly identifies it as a test fixture. No action needed unless your team prefers to add a Gitleaks suppression comment to silence the warning in CI.

Comment on lines +5849 to 5894
const shouldEngage = this._shouldEngageWithThread(evt, threadContext);
if (!shouldEngage) {
logger.debug(`[NOSTR] Home feed skipping reply to ${evt.id.slice(0, 8)} after thread analysis - not suitable for engagement`);
success = false;
break;
}

// Check if we've already replied to this event (early exit to avoid unnecessary LLM calls)
const eventMemoryId = this.createUniqueUuid(this.runtime, evt.id);
const recent = await this.runtime.getMemories({ tableName: 'messages', roomId, count: 100 });
const hasReply = recent.some((m) => m.content?.inReplyTo === eventMemoryId || m.content?.inReplyTo === evt.id);
if (hasReply) {
logger.info(`[NOSTR] Skipping home feed reply to ${evt.id.slice(0, 8)} (found existing reply)`);
success = false;
break;
}

// Process images in home feed post content (if enabled)
let imageContext = { imageDescriptions: [], imageUrls: [] };
if (this.imageProcessingEnabled) {
try {
logger.info(`[NOSTR] Processing images in home feed post: "${evt.content?.slice(0, 200)}..."`);
const { processImageContent } = require('./image-vision');
const fullImageContext = await processImageContent(evt.content || '', this.runtime);
imageContext = {
imageDescriptions: fullImageContext.imageDescriptions.slice(0, this.maxImagesPerMessage),
imageUrls: fullImageContext.imageUrls.slice(0, this.maxImagesPerMessage)
};
logger.info(`[NOSTR] Processed ${imageContext.imageDescriptions.length} images from home feed post`);
} catch (error) {
logger.error(`[NOSTR] Error in home feed image processing: ${error.message || error}`);
imageContext = { imageDescriptions: [], imageUrls: [] };
}
}

const text = await this.generateReplyTextLLM(evt, roomId, threadContext, imageContext);

// Check if LLM generation failed (returned null)
if (!text || !text.trim()) {
logger.warn(`[NOSTR] Skipping home feed reply to ${evt.id.slice(0, 8)} - LLM generation failed`);
success = false;
break;
}
}

const text = await this.generateReplyTextLLM(evt, roomId, threadContext, imageContext);

// Check if LLM generation failed (returned null)
if (!text || !text.trim()) {
logger.warn(`[NOSTR] Skipping home feed reply to ${evt.id.slice(0, 8)} - LLM generation failed`);
success = false;
break;
}

success = await this.postReply(evt, text);

success = await this.postReply(evt, text);
break;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing reply memory makes dedupe ineffective

We now check getMemories() before replying, but nothing in this path ever writes a memory whose content.inReplyTo matches the home‑feed event. After a restart (or whenever homeFeedProcessedEvents is cleared) that query still comes back empty, so the agent will reply again—the original bug remains. Persist the reply the way mention handling already does so the dedupe gate has something to find.

-        const { roomId } = await this._ensureNostrContext(evt.pubkey, undefined, convId);
+        const { roomId, entityId } = await this._ensureNostrContext(evt.pubkey, undefined, convId);
@@
-        success = await this.postReply(evt, text);
+        success = await this.postReply(evt, text);
+        if (success) {
+          try {
+            const replyMemoryId = this.createUniqueUuid(this.runtime, `${evt.id}:reply:${Date.now()}`);
+            await this._createMemorySafe({
+              id: replyMemoryId,
+              entityId,
+              agentId: this.runtime.agentId,
+              roomId,
+              content: {
+                text,
+                source: 'nostr',
+                inReplyTo: eventMemoryId,
+              },
+              createdAt: Date.now(),
+            }, 'messages');
+          } catch (err) {
+            this.logger?.debug?.('[NOSTR] Failed to persist home feed reply memory:', err?.message || err);
+          }
+        }

Committable suggestion skipped: line range outside the PR's diff.

@anabelle
Copy link
Owner Author

@copilot tests are failing

@anabelle anabelle merged commit 648661e into master Oct 18, 2025
2 checks passed
@anabelle anabelle deleted the fix-duplicate-home-feed-replies branch October 18, 2025 14:55
anabelle added a commit that referenced this pull request Dec 10, 2025
* Fix duplicate replies to home feed posts

Add reply deduplication logic in processHomeFeed function to prevent
the agent from replying multiple times to the same home feed post.

The fix checks for existing replies in memory before posting a new
reply, similar to the deduplication logic used for mention handling.

This resolves issue #71: Bug: Duplicate replies to home feed posts

- Added deduplication check before calling postReply in home feed reply logic
- Check looks for existing replies in recent memory (up to 100 messages)
- Uses the same pattern as mention handling for consistency

* Address PR review feedback

- Move deduplication check before LLM call to prevent unnecessary API usage
- Clean up test code by removing console.log statements and refactoring duplicated mocks
- Add helper function for test setup to improve maintainability

* Fix test setup to properly initialize service pool for processHomeFeed
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.

Bug: Duplicate replies to home feed posts

2 participants