Skip to content

Retry transient embedding API failures#476

Closed
mia-fourier wants to merge 1 commit into
mnemosyne-oss:mainfrom
mia-fourier:fix/transient-embedding-retry
Closed

Retry transient embedding API failures#476
mia-fourier wants to merge 1 commit into
mnemosyne-oss:mainfrom
mia-fourier:fix/transient-embedding-retry

Conversation

@mia-fourier

@mia-fourier mia-fourier commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Description

This change adds bounded retry handling for transient embedding endpoint failures in _embed_api.

  • retries URL, network, timeout, and connection errors
  • retries HTTP 429 and HTTP 5xx
  • preserves no-retry behavior for non-transient client errors
  • adds regression coverage for network, timeout, HTTP 429, HTTP 503, non-transient HTTP 400, and exhausted transient attempts

Related Issue

Closes #475

Type of Change

  • Bug fix
  • Test improvement

How Has This Been Tested?

  • New focused tests: 5 passed
  • Embedding-related unit tests: 38 passed
  • Manual BGE transport verification
  • git diff --check

The manual check used the cached quantized BAAI/bge-small-en-v1.5 ONNX model behind a temporary loopback OpenAI-compatible endpoint. The endpoint returned HTTP 503 for the first request and a real 384-dimensional embedding for the retry.

The current upstream main full suite has an unrelated baseline failure after 85 passes in tests/test_beam.py::TestMnemosyneIntegration::test_legacy_and_beam_dual_write: Mnemosyne has no beam attribute and __del__ references an undefined session_id. This branch does not modify mnemosyne/core/memory.py.

Checklist

  • Version bumped in mnemosyne/__init__.py (maintainer release task)
  • CHANGELOG.md updated (maintainer release task)
  • No new runtime dependency
  • No new cloud or API-key requirement
  • SQLite remains the only database dependency

Summary

Adds bounded retries for transient embedding API failures, including network/timeouts, HTTP 429, and HTTP 5xx responses. Retries use exponential backoff with jitter and stop after three attempts, while non-transient errors such as HTTP 400 fail immediately.

Architectural impact

  • Improves embedding reliability and prevents transient failures from creating silent retrieval gaps.
  • Does not change working, episodic, or BEAM memory tiers, retrieval strategies, consolidation, veracity, or sync behavior.
  • Preserves local-first behavior: no new runtime dependencies, databases, or remote services are introduced. The change only retries requests when an external embedding API is already configured.
  • Does not alter Hermes, MCP, CLI, or other agent integration surfaces.
  • No privacy posture change; retry behavior does not expand data handling or storage.
  • Adds focused regression coverage for transient errors, non-retryable client errors, backoff timing, and exhausted retries, improving long-term maintainability.

This is the right call for resilience: bounded, jittered retries address transient provider failures without masking permanent client errors or changing Mnemosyne’s core architecture.

@CLAassistant

CLAassistant commented Jul 16, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

_embed_api now classifies transient embedding failures and retries network, timeout, rate-limit, and server errors with bounded exponential backoff and jitter. New tests verify retry counts, delays, and non-retryable client errors.

Changes

Embedding retry handling

Layer / File(s) Summary
Transient embedding error classification
mnemosyne/core/embeddings.py
Adds transient-error detection for rate limits, HTTP 5xx responses, network failures, timeouts, and operating-system errors.
Bounded retry and backoff behavior
mnemosyne/core/embeddings.py, tests/test_embedding_api_retry.py
Updates _embed_api to retry transient failures for up to three attempts with jittered exponential delays, with tests covering transient, non-transient, and exhausted-retry cases.

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

Suggested reviewers: axdsan, flooryyyy

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: bounded retries for transient embedding API failures.
Linked Issues check ✅ Passed The PR implements bounded retries with backoff and jitter for transient embedding errors and skips non-transient 400s, matching #475.
Out of Scope Changes check ✅ Passed The changes stay focused on embedding retry logic and regression tests, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_embedding_api_retry.py (1)

1-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Good assertions, but coverage gaps remain in _is_transient_embedding_error's classification surface.

All four tests assert concrete outcomes (call counts, exact sleep values, returned data) rather than just "doesn't crash" — solid. However, the classifier being tested (_is_transient_embedding_error) has three distinct branches and this suite only exercises two of them:

  • The string-match fallback branch ("429"/"rate limit"/"rate-limit" in str(exc) for exceptions that aren't HTTPError/URLError/TimeoutError/ConnectionError/OSError) is never exercised by any test here.
  • Bare ConnectionError/generic OSError raised directly (not wrapped in URLError) aren't tested, even though the classifier treats them as transient.
  • The HTTP-transient boundary is only tested at 429/503 — the edges of 500 <= exc.code < 600 (500 and 599) aren't tested.
  • Non-transient coverage is only at 400 — no test for a non-transient status just outside the transient range (e.g. 600) or another common 4xx (e.g. 404).

Given this suite exists specifically to validate the retry/backoff and classification contract, closing these gaps would meaningfully strengthen confidence in _is_transient_embedding_error's edge behavior.

As per path instructions, "Flag missing or weak test scenarios" and "Verify that new tests actually assert something meaningful" for files under tests/**.

🤖 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 `@tests/test_embedding_api_retry.py` around lines 1 - 81, Extend the tests
covering embeddings._is_transient_embedding_error to exercise its string-match
fallback with a non-specialized exception, direct ConnectionError and OSError
cases, HTTP status boundaries 500 and 599, and non-transient statuses 404 and
600. Keep assertions explicit about classification or resulting retry behavior,
reusing the existing retry test setup and avoiding tests that only verify the
call does not raise.

Source: Path instructions

🤖 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 `@mnemosyne/core/embeddings.py`:
- Around line 237-244: Update _is_transient_embedding_error to reuse the
existing _is_rate_limit_error helper for its fallback classification instead of
duplicating message matching, preserving the helper’s handling of “too many
requests” and other rate-limit cases while leaving HTTP and transport-error
handling unchanged.

---

Outside diff comments:
In `@tests/test_embedding_api_retry.py`:
- Around line 1-81: Extend the tests covering
embeddings._is_transient_embedding_error to exercise its string-match fallback
with a non-specialized exception, direct ConnectionError and OSError cases, HTTP
status boundaries 500 and 599, and non-transient statuses 404 and 600. Keep
assertions explicit about classification or resulting retry behavior, reusing
the existing retry test setup and avoiding tests that only verify the call does
not raise.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1615d9e6-444a-488e-9731-fd9bec35d9a3

📥 Commits

Reviewing files that changed from the base of the PR and between 0a64d2e and 0aacd9b.

📒 Files selected for processing (2)
  • mnemosyne/core/embeddings.py
  • tests/test_embedding_api_retry.py

Comment on lines +237 to +244
def _is_transient_embedding_error(exc: Exception) -> bool:
"""Return whether an API embedding request is safe to retry."""
if isinstance(exc, urllib.error.HTTPError):
return exc.code == 429 or 500 <= exc.code < 600
if isinstance(exc, (urllib.error.URLError, TimeoutError, ConnectionError, OSError)):
return True
message = str(exc).lower()
return "429" in message or "rate limit" in message or "rate-limit" in message

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 | 🟠 Major | ⚡ Quick win

Duplicates and diverges from the existing _is_rate_limit_error helper.

The fallback branch reimplements the rate-limit string matching that _is_rate_limit_error (Line 221) already provides, but drops the "too many requests" case that the existing helper checks. This creates two slightly different classifiers for the same concept — an embedding API error whose message is "Too many requests" (no "429" or "rate limit" phrase) won't be retried here even though _is_rate_limit_error would flag it as transient.

♻️ Proposed fix: reuse the existing helper
 def _is_transient_embedding_error(exc: Exception) -> bool:
     """Return whether an API embedding request is safe to retry."""
     if isinstance(exc, urllib.error.HTTPError):
         return exc.code == 429 or 500 <= exc.code < 600
     if isinstance(exc, (urllib.error.URLError, TimeoutError, ConnectionError, OSError)):
         return True
-    message = str(exc).lower()
-    return "429" in message or "rate limit" in message or "rate-limit" in message
+    return _is_rate_limit_error(exc)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _is_transient_embedding_error(exc: Exception) -> bool:
"""Return whether an API embedding request is safe to retry."""
if isinstance(exc, urllib.error.HTTPError):
return exc.code == 429 or 500 <= exc.code < 600
if isinstance(exc, (urllib.error.URLError, TimeoutError, ConnectionError, OSError)):
return True
message = str(exc).lower()
return "429" in message or "rate limit" in message or "rate-limit" in message
def _is_transient_embedding_error(exc: Exception) -> bool:
"""Return whether an API embedding request is safe to retry."""
if isinstance(exc, urllib.error.HTTPError):
return exc.code == 429 or 500 <= exc.code < 600
if isinstance(exc, (urllib.error.URLError, TimeoutError, ConnectionError, OSError)):
return True
return _is_rate_limit_error(exc)
🤖 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 `@mnemosyne/core/embeddings.py` around lines 237 - 244, Update
_is_transient_embedding_error to reuse the existing _is_rate_limit_error helper
for its fallback classification instead of duplicating message matching,
preserving the helper’s handling of “too many requests” and other rate-limit
cases while leaving HTTP and transport-error handling unchanged.

AxDSan added a commit that referenced this pull request Jul 16, 2026
@AxDSan

AxDSan commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Merged in 6b4de5f. @mia-fourier thanks for the retry tests. The retry logic already shipped in #478 (raitoxlol), but your focused test coverage is a valuable addition. Your second contribution after #206 (embedding API URL independence, back in May). Solid follow-up.

Edit: @mia-fourier — I originally called this your "first contribution" which is wrong. It's your second. Your first was #206. My bad, not tracking contributor history properly.

@mia-fourier

Copy link
Copy Markdown
Contributor Author

@AxDSan It's actually my second, but thanks for the recognition. I have a few other ideas I'm thinking about posting as issues fro feature requests to provide fine grained access control over memory. Will post more when I have it fleshed out, but I think it's going to be a good one. Think "Private", "Confidential", "Public" there's a bit more I'm putting together a spec and design document, but it's on the back burner while a few other things I am working on get released.

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.

Transient embedding API failures are not retried consistently

3 participants