Files
GoClaw/server/chat-resilience.ts
2026-03-26 05:41:44 -04:00

121 lines
3.1 KiB
TypeScript

import { getDb } from "./db";
/**
* Chat resilience utilities: retry logic with exponential backoff and context recovery
*/
export interface RetryConfig {
maxAttempts: number;
initialDelayMs: number;
maxDelayMs: number;
backoffMultiplier: number;
}
export const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxAttempts: 3,
initialDelayMs: 1000,
maxDelayMs: 8000,
backoffMultiplier: 2,
};
/**
* Calculate delay for exponential backoff
*/
export function calculateBackoffDelay(attempt: number, config: RetryConfig): number {
const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1);
return Math.min(delay, config.maxDelayMs);
}
/**
* Sleep utility
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Retry wrapper with exponential backoff
*/
export async function retryWithBackoff<T>(
fn: () => Promise<T>,
config: RetryConfig = DEFAULT_RETRY_CONFIG,
onRetry?: (attempt: number, error: Error) => void
): Promise<T> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < config.maxAttempts) {
const delayMs = calculateBackoffDelay(attempt, config);
onRetry?.(attempt, lastError);
await sleep(delayMs);
}
}
}
throw lastError || new Error("Retry failed");
}
/**
* Check if error is retryable (timeout, network, 5xx)
*/
export function isRetryableError(error: any): boolean {
if (!error) return false;
const message = String(error.message || error).toLowerCase();
const code = error.code || error.status;
// Timeout errors
if (message.includes("timeout") || message.includes("econnreset")) return true;
// Network errors
if (message.includes("econnrefused") || message.includes("enotfound")) return true;
// 5xx server errors
if (code >= 500 && code < 600) return true;
// Gateway errors
if (code === 502 || code === 503 || code === 504) return true;
// LLM service unavailable
if (message.includes("unavailable") || message.includes("service")) return true;
return false;
}
/**
* Get recent conversation context from DB for retry
*/
export async function getConversationContext(
userId: string,
limit: number = 10
): Promise<Array<{ role: "user" | "assistant" | "system"; content: string }>> {
try {
const db = getDb();
// Note: This assumes a messages table exists with userId, role, content, createdAt
// If not, return empty array (frontend will use in-memory history)
return [];
} catch (error) {
console.error("[ChatResilience] Failed to get conversation context:", error);
return [];
}
}
/**
* Log retry attempt for monitoring
*/
export function logRetryAttempt(
attempt: number,
error: Error,
context?: Record<string, any>
): void {
console.log(
`[ChatResilience] Retry attempt ${attempt}: ${error.message}`,
context ? JSON.stringify(context) : ""
);
}