Skip to content

fix(core): prevent duplicate LLM calls when message contains URL or triggers providers#6528

Open
nicolasdma wants to merge 216 commits intoelizaOS:v2.0.0from
nicolasdma:fix/duplicate-llm-calls-url-messages
Open

fix(core): prevent duplicate LLM calls when message contains URL or triggers providers#6528
nicolasdma wants to merge 216 commits intoelizaOS:v2.0.0from
nicolasdma:fix/duplicate-llm-calls-url-messages

Conversation

@nicolasdma
Copy link

@nicolasdma nicolasdma commented Feb 23, 2026

Relates to

Relates to #6486

Risks

Low — single condition removed from a boolean expression. No new code, no new logic paths.

Background

What does this PR do?

Removes the providers.length check from the isSimple determination in packages/typescript/src/services/message.ts.

What kind of change is this?

Bug fix (non-breaking change that fixes an issue).

Changes

When a user sends a message containing a URL, the LLM's first call (via runSingleShotCore) generates and streams the response text. It also returns providers: ["ATTACHMENTS"] because the prompt template instructs it to when it sees URLs.

The isSimple check at line 1883 previously treated "has providers" as "not simple":

const isSimple =
    responseContent?.actions &&
    responseContent.actions.length === 1 &&
    typeof responseContent.actions[0] === "string" &&
    responseContent.actions[0].toUpperCase() === "REPLY" &&
    (!responseContent.providers || responseContent.providers.length === 0);
//   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//   This condition causes the bug

This caused the message to go through the actions path, which triggered the REPLY action handler (reply.ts:60), making a second LLM call with replyTemplate and streaming another response — duplicating output and doubling token costs.

The fix: Remove the providers check. Providers enrich state (already handled at L863-871) but don't require re-generating the response. isSimple should only care about whether the action is REPLY (text already generated and streamed) vs something else (needs execution).

const isSimple =
    responseContent?.actions &&
    responseContent.actions.length === 1 &&
    typeof responseContent.actions[0] === "string" &&
    responseContent.actions[0].toUpperCase() === "REPLY";

Testing

  • Message with URL → single response (not duplicated)
  • Message without URL → works as before
  • Multi-action messages (REPLY + other) → still trigger actions path
  • Messages with non-REPLY actions → still trigger actions path

Greptile Summary

Removes the providers.length check from the isSimple boolean expression, preventing duplicate LLM calls when messages contain URLs or trigger provider enrichment.

Key Changes:

  • Removed condition (!responseContent.providers || responseContent.providers.length === 0) from isSimple check at line 1883
  • Messages with actions: ["REPLY"] now correctly use the "simple" path regardless of provider presence
  • Providers still enrich state (lines 863-871) but no longer force re-generation of already-streamed responses

Impact:

  • Eliminates duplicate streaming when URLs are detected (previously triggered ATTACHMENTS provider → action path → second LLM call)
  • Reduces token costs by avoiding unnecessary second LLM invocation
  • Response text from first runSingleShotCore call is now correctly used as final output

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • Simple, surgical fix that removes a single condition causing unintended behavior. The logic is sound: providers enrich state (already handled separately at L863-871) but shouldn't force action-based execution when the response is already generated and streamed. No new code paths, no breaking changes.
  • No files require special attention

Important Files Changed

Filename Overview
packages/typescript/src/services/message.ts Removed providers check from isSimple determination to prevent duplicate LLM calls when URLs trigger provider enrichment

Last reviewed commit: 681b3d8

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

wtfsayo and others added 30 commits December 29, 2025 08:50
Implement comprehensive hot reload functionality for backend code changes
during development. When TypeScript files in watched packages are modified,
the system automatically rebuilds the CLI and restarts the server.

Core implementation:
- Watch all CLI dependency packages (cli, core, server, api-client, plugin-bootstrap, plugin-sql, config)
- Debounce file changes (300ms) to prevent rapid rebuilds
- Graceful server shutdown and restart on code changes
- Health check verification after rebuilds to detect crashes
- Rebuild queueing for changes during active rebuilds

Technical details:
- Use Bun.spawn() for all process execution per project standards
- Comprehensive TypeScript type annotations with JSDoc
- Exit event listeners for proper SIGKILL fallback
- Directory existence checks to handle optional packages gracefully
- Full test coverage with 13 passing tests using temp directories

The dev environment now provides:
- Automatic backend rebuild on TypeScript file changes
- Server health verification after each rebuild
- Graceful error handling and process cleanup
- Clear logging for all rebuild operations

Usage: bun run dev

Addresses all PR review feedback from @cursor, @greptile-apps, and @claude
Co-authored-by: sayonara <sayonara@elizalabs.ai>
- Add .catch() handler on child.exited promise
- Ensure setTimeout always calls resolve() after SIGKILL
- Wrap child.kill() in try-catch for error handling
- Match the pattern used in cleanup() function

This prevents the rebuild from freezing if the process doesn't
respond to signals or if child.exited rejects.
Co-authored-by: sayonara <sayonara@elizalabs.ai>
…r extraction

- Add retry logic for XML parsing with exponential backoff (1s, 2s, 4s... capped at 8s)
- Add summary generation retry with graceful fallback message
- Add parameter extraction from multi-step decision template
- Add formatActionsWithParams() for LLM parameter schema information
- Add bounds checking for retry counts (1-10 range)
- Add comprehensive tests for retry and parameter scenarios
Enhanced JSON parameter parsing to ensure only non-null objects are accepted, with appropriate logging for invalid types. Updated related test and provider logic to match stricter validation and improved code consistency.
Added explicit checks and warnings for array-type parameters in both DefaultMessageService and multi-step tests. Metadata properties in accumulated state are now prefixed with underscores to avoid collisions with action parameters.
Improves logging granularity and clarity for multi-step message processing, including step-by-step info, provider/action execution, and summary generation. Adds streaming support for summary output in multi-step mode and ensures streaming contexts are correctly managed for both single-shot and multi-step flows. Minor code formatting and consistency improvements applied.
Replaces 'state' with 'accumulatedState' in provider and action calls to ensure correct state is used. Adds logic to prevent duplicate streaming of summary output to the user on retry attempts, tracking streaming status with 'hasStreamedToUser'.
Marks streaming as started before sending content to prevent duplicate streams on retries in DefaultMessageService. Updates action parameter formatting to handle invalid definitions and provide defaults for missing type or description.
Updates submodule from 797ad22 (init commit) to 4a33c0c
(Add ElizaOS scenario testing rules)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…entries

- Add array check to prevent malformed parameter output
- Filter invalid param entries upfront to avoid dangling headers
- Added methods to StreamingContext for managing streaming state, including reset, getStreamedText, and isComplete.
- Improved DefaultMessageService to utilize intelligent retry logic for streaming, allowing for continuation prompts when text extraction is incomplete.
- Updated tests to validate new streaming context features and ensure correct behavior during retries.
- Enhanced IStreamExtractor interface with reset and flush methods for better state management.
- Removed redundant '[MULTI-STEP]' prefix from logging messages in DefaultMessageService to enhance clarity and readability.
- Updated various log statements to maintain consistency while preserving essential information about the multi-step processing flow.
- other: fix docs
Replace ACTIONS_REGEX with indexOf-based extraction in detectResponseStrategy.
This eliminates potential polynomial-time regex matching on malicious inputs.
Ensures continuation response is properly streamed to user in single-shot
mode, matching the behavior of multi-step continuation at line 1862.
…nt results

Previously, calling getStreamedText() multiple times would return different
results because flush() empties the buffer. Now the flushed content is
accumulated into streamedText, ensuring consistent results across calls.
This commit refactors the error handling and shutdown logic in the development server scripts. It ensures that processes are properly cleaned up and that errors are logged and handled more effectively.

Co-authored-by: sayonara <sayonara@elizalabs.ai>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 23, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, no comments

Edit Code Review Agent Settings | Greptile

…riggers providers

Remove providers length check from isSimple determination. Providers
enrich state (already handled at L863-871) but don't require
re-generating the response. The previous check incorrectly treated
"has providers" as "not simple", causing messages with URLs to go
through the actions path and trigger a second LLM call via the REPLY
action handler.

Relates to elizaOS#6486
@nicolasdma nicolasdma force-pushed the fix/duplicate-llm-calls-url-messages branch from 681b3d8 to 3a304b6 Compare February 23, 2026 12:02
@odilitime
Copy link
Collaborator

if it has providers, it's not simple. That is correct.

@nicolasdma
Copy link
Author

The ATTACHMENTS provider gets set during the initial runSingleShotCore pass (after URL detection).
But by the time we evaluate isSimple, the reply has already been generated and streamed.

In reply.ts around line 70: is the second LLM call expected when actions: ["REPLY"] and providers are present?
Right now I’m seeing duplicated output and ~2x token usage. If that’s intentional, I can close this PR

@odilitime odilitime added the 2.x V3 label Mar 4, 2026
@odilitime odilitime changed the base branch from develop to v2.0.0 March 4, 2026 20:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.