4316dbd3be
- Extract shared splitMessage() into channels/utils.ts (dedup 4 adapters) - Add Slack user name resolution with caching (users.info API) - Add withRetry() with exponential backoff + jitter, isRetryable() filter - Wire retry config into ModelRouter.chat() (non-streaming only) - Add assembleSystemPrompt() multi-file template system (SOUL/AGENTS/IDENTITY/USER/TOOLS.md) - Add usage tracking accumulators in NativeAgent + AgentOrchestrator - Add estimateCost() with per-model pricing table - Add /usage TUI command with full usage report formatting - Add retrySchema and promptSchema to config schema Tests: 569 passing, typecheck clean
72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
export interface RetryConfig {
|
|
/** Maximum number of retry attempts (default: 3). Does not count the initial attempt. */
|
|
maxRetries: number;
|
|
/** Initial delay in milliseconds before first retry (default: 1000). */
|
|
initialDelayMs: number;
|
|
/** Multiplier applied to delay after each retry (default: 2). */
|
|
backoffMultiplier: number;
|
|
/** Maximum delay in milliseconds (default: 30000). */
|
|
maxDelayMs: number;
|
|
/** Errors matching these patterns should NOT be retried (e.g. auth errors, invalid requests). */
|
|
nonRetryablePatterns: string[];
|
|
}
|
|
|
|
export const DEFAULT_RETRY_CONFIG: RetryConfig = {
|
|
maxRetries: 3,
|
|
initialDelayMs: 1000,
|
|
backoffMultiplier: 2,
|
|
maxDelayMs: 30000,
|
|
nonRetryablePatterns: [
|
|
'invalid_api_key',
|
|
'authentication',
|
|
'unauthorized',
|
|
'invalid_request',
|
|
'context_length_exceeded',
|
|
'content_policy',
|
|
],
|
|
};
|
|
|
|
export function isRetryable(error: Error, nonRetryablePatterns: string[]): boolean {
|
|
const msg = error.message.toLowerCase();
|
|
return !nonRetryablePatterns.some(pattern => msg.includes(pattern.toLowerCase()));
|
|
}
|
|
|
|
export async function withRetry<T>(
|
|
fn: () => Promise<T>,
|
|
config: RetryConfig = DEFAULT_RETRY_CONFIG,
|
|
label?: string,
|
|
): Promise<T> {
|
|
let lastError: Error | undefined;
|
|
|
|
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
try {
|
|
return await fn();
|
|
} catch (error) {
|
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
|
|
// Don't retry non-retryable errors
|
|
if (!isRetryable(lastError, config.nonRetryablePatterns)) {
|
|
throw lastError;
|
|
}
|
|
|
|
// Don't retry if we've exhausted attempts
|
|
if (attempt >= config.maxRetries) {
|
|
throw lastError;
|
|
}
|
|
|
|
// Calculate delay with exponential backoff + jitter
|
|
const baseDelay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt);
|
|
const delay = Math.min(baseDelay, config.maxDelayMs);
|
|
const jitter = delay * (0.5 + Math.random() * 0.5); // 50-100% of delay
|
|
|
|
console.warn(
|
|
`[retry] ${label ?? 'operation'} attempt ${attempt + 1}/${config.maxRetries} failed: ${lastError.message}. Retrying in ${Math.round(jitter)}ms...`,
|
|
);
|
|
|
|
await new Promise(resolve => setTimeout(resolve, jitter));
|
|
}
|
|
}
|
|
|
|
throw lastError ?? new Error('Retry failed with no error');
|
|
}
|