Skip to content

fix(embedding): retry transient remote embedding failures (fixes import timeout #400) - #401

Merged
EyJunge1 merged 2 commits into
tickernelz:mainfrom
nnikolov3:fix/embedding-remote-retry
Oct 12, 2026
Merged

EyJunge1 merged 2 commits into
tickernelz:mainfrom
nnikolov3:fix/embedding-remote-retry

Conversation

@nnikolov3

Copy link
Copy Markdown
Contributor

Fixes #400.

Root cause

With remote embeddings configured, a single hung HTTP request anywhere in the import path aborted the whole import with Timeout after 30000ms and zero memories written. Two exposed spots:

  • initializeModel() reachability probe: one fetch under the 30s TIMEOUT_MS, no retry — and its failure sets the sticky initError until restart.
  • embed() remote fetch: no per-request timeout/retry; one hang inside import Phase A's sequential embed loop discarded all progress.

Reproduced deterministically by injecting one 40s fetch hang: {"success":false,"imported":0,"error":"Timeout after 30000ms"}.

Fix

New fetchRemoteEmbeddingWithRetry(): per-attempt 10s ceiling, up to 3 attempts, linear backoff, retry limited to network errors / per-attempt timeouts / 429 / 5xx; other 4xx fail fast. Both the probe and embed() route through it, so add, search-query, and import all gain transient-failure resilience while staying inside the existing outer embedWithTimeout budget.

Evidence

  • Same injected-hang repro after the fix: {"success":true,"imported":3} in ~12s (hang absorbed on attempt 2).
  • New tests tests/embedding-remote-retry.test.ts (6 cases: hang recovery, 429, 500, fail-fast 401, attempt exhaustion, request shape).
  • tsc --noEmit, eslint . --max-warnings=0, prettier clean; full bun test failure set identical to clean main in this environment (15 pre-existing environmental failures, none related).

Notes

  • Healthy-endpoint behavior unchanged (single attempt, ~0.3–0.6s per embed).
  • Worst case for a dead endpoint stays bounded: ~31.5s across attempts, surfaced through the existing error path.

nnikolov3 and others added 2 commits October 11, 2026 18:36
Problem: a single hung remote embedding request aborted an entire
import with zero memories written (issue tickernelz#400, reproduced 2x) and
could wedge search the same way. The warmup probe had no retry either,
and its failure set the sticky initError until restart.

Design: extract the remote call into fetchRemoteEmbeddingWithRetry —
per-attempt 10s ceiling, up to 3 attempts, linear backoff, retry only
network errors/timeouts/429/5xx, fail fast on other 4xx. Both the
warmup probe and embed() now route through it, so add, search-query,
and import all gain transient-failure resilience inside the existing
outer embedWithTimeout budget.

Verification: new tests/embedding-remote-retry.test.ts (6 cases:
hang-recovery, 429, 500, fail-fast 401, exhaustion, request shape);
tsc --noEmit, eslint, prettier clean; full bun test failure set
identical to clean main (15 pre-existing environmental failures).
End-to-end: instrumented import with one injected 40s fetch hang —
before: Timeout after 30000ms, imported 0; after: success, imported 3.

Consequences: worst-case latency for a dead endpoint rises from 30s
per call to ~31.5s across attempts but stays bounded; healthy-endpoint
behavior unchanged (single attempt).
Use AbortSignal.timeout per attempt so hung fetches are cancelled instead
of orphaned, keep 3×8s under the outer 30s budget, and whitelist only
transient errors for retry (429/5xx, abort/timeout, network TypeError).
@EyJunge1
EyJunge1 merged commit 337236d into tickernelz:main Oct 12, 2026
13 checks passed
@EyJunge1 EyJunge1 mentioned this pull request Oct 12, 2026
2 of 9 tasks
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.

Import times out after 30s and imports 0 memories with remote embeddings (reproduced 2x)

2 participants