From 7d2e5100878a1dd594392f7aa4c6a7aebc62e86b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 17 Jan 2026 22:48:50 +0000 Subject: [PATCH] fix: retry embedding 5xx errors --- CHANGELOG.md | 3 ++ src/memory/manager.batch.test.ts | 9 ++++- src/memory/manager.embedding-batches.test.ts | 41 ++++++++++++++++++++ src/memory/manager.ts | 12 +++--- 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54a074d7f..f00c0f346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ Docs: https://docs.clawd.bot ### Changes - Memory: add OpenAI Batch API indexing for embeddings when configured. +### Fixes +- Memory: retry transient 5xx errors (Cloudflare) during embedding indexing. + ## 2026.1.17-2 ### Changes diff --git a/src/memory/manager.batch.test.ts b/src/memory/manager.batch.test.ts index 7547a8b13..a6c5fd5e2 100644 --- a/src/memory/manager.batch.test.ts +++ b/src/memory/manager.batch.test.ts @@ -138,11 +138,18 @@ describe("memory indexing with OpenAI batches", () => { expect(result.manager).not.toBeNull(); if (!result.manager) throw new Error("manager missing"); manager = result.manager; - await manager.sync({ force: true }); + const labels: string[] = []; + await manager.sync({ + force: true, + progress: (update) => { + if (update.label) labels.push(update.label); + }, + }); const status = manager.status(); expect(status.chunks).toBeGreaterThan(0); expect(embedBatch).not.toHaveBeenCalled(); expect(fetchMock).toHaveBeenCalled(); + expect(labels.some((label) => label.toLowerCase().includes("batch"))).toBe(true); }); }); diff --git a/src/memory/manager.embedding-batches.test.ts b/src/memory/manager.embedding-batches.test.ts index 6a0b7e505..01dc31983 100644 --- a/src/memory/manager.embedding-batches.test.ts +++ b/src/memory/manager.embedding-batches.test.ts @@ -192,6 +192,47 @@ describe("memory embedding batches", () => { expect(calls).toBe(3); }, 10000); + it("retries embeddings on transient 5xx errors", async () => { + const line = "e".repeat(120); + const content = Array.from({ length: 12 }, () => line).join("\n"); + await fs.writeFile(path.join(workspaceDir, "memory", "2026-01-08.md"), content); + + let calls = 0; + embedBatch.mockImplementation(async (texts: string[]) => { + calls += 1; + if (calls < 3) { + throw new Error("openai embeddings failed: 502 Bad Gateway (cloudflare)"); + } + return texts.map(() => [0, 1, 0]); + }); + + const cfg = { + agents: { + defaults: { + workspace: workspaceDir, + memorySearch: { + provider: "openai", + model: "mock-embed", + store: { path: indexPath }, + chunking: { tokens: 200, overlap: 0 }, + sync: { watch: false, onSessionStart: false, onSearch: false }, + query: { minScore: 0 }, + }, + }, + list: [{ id: "main", default: true }], + }, + }; + + const result = await getMemorySearchManager({ cfg, agentId: "main" }); + expect(result.manager).not.toBeNull(); + if (!result.manager) throw new Error("manager missing"); + manager = result.manager; + + await manager.sync({ force: true }); + + expect(calls).toBe(3); + }, 10000); + it("skips empty chunks so embeddings input stays valid", async () => { await fs.writeFile(path.join(workspaceDir, "memory", "2026-01-07.md"), "\n\n\n"); diff --git a/src/memory/manager.ts b/src/memory/manager.ts index 514ab395c..303e57136 100644 --- a/src/memory/manager.ts +++ b/src/memory/manager.ts @@ -733,7 +733,7 @@ export class MemoryIndexManager { params.progress.report({ completed: params.progress.completed, total: params.progress.total, - label: "Indexing memory files…", + label: this.batch.enabled ? "Indexing memory files (batch)..." : "Indexing memory files…", }); } @@ -784,7 +784,7 @@ export class MemoryIndexManager { params.progress.report({ completed: params.progress.completed, total: params.progress.total, - label: "Indexing session files…", + label: this.batch.enabled ? "Indexing session files (batch)..." : "Indexing session files…", }); } @@ -1357,7 +1357,7 @@ export class MemoryIndexManager { return await this.provider.embedBatch(texts); } catch (err) { const message = err instanceof Error ? err.message : String(err); - if (!this.isRateLimitError(message) || attempt >= EMBEDDING_RETRY_MAX_ATTEMPTS) { + if (!this.isRetryableEmbeddingError(message) || attempt >= EMBEDDING_RETRY_MAX_ATTEMPTS) { throw err; } const waitMs = Math.min( @@ -1372,8 +1372,10 @@ export class MemoryIndexManager { } } - private isRateLimitError(message: string): boolean { - return /(rate[_ ]limit|too many requests|429|resource has been exhausted)/i.test(message); + private isRetryableEmbeddingError(message: string): boolean { + return /(rate[_ ]limit|too many requests|429|resource has been exhausted|5\d\d|cloudflare)/i.test( + message, + ); } private async runWithConcurrency(tasks: Array<() => Promise>, limit: number): Promise {