From 3034accd752d6009d044ca29d7c3a0a5c734d501 Mon Sep 17 00:00:00 2001 From: ronitchidara Date: Wed, 28 Jan 2026 17:41:49 +0530 Subject: [PATCH] fix: preserve batch failure state across EmbeddingService recreations - Add transferBatchState() method to EmbeddingService to preserve failure tracking when the service is recreated during reindex operations - Update manager.createEmbeddingService() to transfer state by default - Skip state transfer when switching to fallback provider (fresh start) - Add mock rateLimiter to send.test.ts to fix test failures - Add error handling in audit-log.ts for ENOENT during test cleanup Co-Authored-By: Claude Opus 4.5 --- src/gateway/server-methods/send.test.ts | 5 +++++ src/memory/embedding-service.ts | 14 ++++++++++++++ src/memory/manager.ts | 12 +++++++++--- src/security/audit-log.ts | 21 +++++++++++++++------ 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/gateway/server-methods/send.test.ts b/src/gateway/server-methods/send.test.ts index 8c50da881..0c3fbcd71 100644 --- a/src/gateway/server-methods/send.test.ts +++ b/src/gateway/server-methods/send.test.ts @@ -45,6 +45,11 @@ vi.mock("../../config/sessions.js", async () => { const makeContext = (): GatewayRequestContext => ({ dedupe: new Map(), + rateLimiter: { + checkChannelMessage: () => ({ allowed: true }), + checkRequest: () => ({ allowed: true }), + recordAuthFailure: () => {}, + }, }) as unknown as GatewayRequestContext; describe("gateway send mirroring", () => { diff --git a/src/memory/embedding-service.ts b/src/memory/embedding-service.ts index 5acdbd5c3..2fcef7de3 100644 --- a/src/memory/embedding-service.ts +++ b/src/memory/embedding-service.ts @@ -122,6 +122,20 @@ export class EmbeddingService { }; } + /** + * Transfer batch failure state from another EmbeddingService instance. + * Used when recreating the service to preserve failure tracking across reindexes. + */ + transferBatchState(from: EmbeddingService): void { + this.batchFailureCount = from.batchFailureCount; + this.batchFailureLastError = from.batchFailureLastError; + this.batchFailureLastProvider = from.batchFailureLastProvider; + // Also transfer disabled state if the source had batch disabled due to failures + if (!from.batch.enabled && from.batchFailureCount >= BATCH_FAILURE_LIMIT) { + this.batch.enabled = false; + } + } + /** * Get cache entry count. */ diff --git a/src/memory/manager.ts b/src/memory/manager.ts index 7a2eca3b1..db4baa6c3 100644 --- a/src/memory/manager.ts +++ b/src/memory/manager.ts @@ -1156,7 +1156,7 @@ export class MemoryIndexManager { return /embedding|embeddings|batch/i.test(message); } - private createEmbeddingService(): EmbeddingService { + private createEmbeddingService(preserveBatchState = true): EmbeddingService { const batch = this.settings.remote?.batch; const enabled = Boolean( batch?.enabled && @@ -1178,7 +1178,12 @@ export class MemoryIndexManager { }, agentId: this.agentId, }; - return new EmbeddingService(this.db, config); + const service = new EmbeddingService(this.db, config); + // Preserve batch failure state across service recreations (e.g., during reindex) + if (preserveBatchState && this.embeddingService) { + service.transferBatchState(this.embeddingService); + } + return service; } private async activateFallbackProvider(reason: string): Promise { @@ -1210,7 +1215,8 @@ export class MemoryIndexManager { this.openAi = fallbackResult.openAi; this.gemini = fallbackResult.gemini; this.providerKey = this.computeProviderKey(); - this.embeddingService = this.createEmbeddingService(); + // Don't preserve batch state when switching providers - start fresh with new provider + this.embeddingService = this.createEmbeddingService(false); log.warn(`memory embeddings: switched to fallback provider (${fallback})`, { reason }); return true; } diff --git a/src/security/audit-log.ts b/src/security/audit-log.ts index 2e8cf463a..36ce5fe4a 100644 --- a/src/security/audit-log.ts +++ b/src/security/audit-log.ts @@ -110,14 +110,23 @@ function getArchivePath(basePath: string, date: Date): string { async function appendAuditEntry(entry: AuditLogEntry, baseDir?: string): Promise { const { dir, currentPath } = resolveAuditPaths(baseDir); - // Ensure directory exists - await fs.mkdir(dir, { recursive: true }); + try { + // Ensure directory exists + await fs.mkdir(dir, { recursive: true }); - // Serialize entry - const line = JSON.stringify(entry) + "\n"; + // Serialize entry + const line = JSON.stringify(entry) + "\n"; - // Append to file (create if doesn't exist) - await fs.appendFile(currentPath, line, { encoding: "utf8", mode: 0o600 }); + // Append to file (create if doesn't exist) + await fs.appendFile(currentPath, line, { encoding: "utf8", mode: 0o600 }); + } catch (err) { + // Silently ignore errors if the directory was removed (e.g., during test cleanup) + // This prevents unhandled rejections when the audit log is disabled or the path is invalid + const code = (err as NodeJS.ErrnoException).code; + if (code !== "ENOENT" && code !== "ENOTDIR") { + throw err; + } + } } /**