UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

233 lines (232 loc) 6.64 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { spawn } from "child_process"; import { loadModelRouterConfig, buildModelEnv } from "./model-router.js"; const DEFAULT_ERROR_PATTERNS = [ /rate.?limit/i, /429/, /too.?many.?requests/i, /overloaded/i, /capacity/i, /temporarily.?unavailable/i, /503/, /502/, /500/, /internal.?server.?error/i, /timeout/i, /ETIMEDOUT/, /ESOCKETTIMEDOUT/, /ECONNRESET/, /ECONNREFUSED/ ]; class FallbackMonitor { config; routerConfig = loadModelRouterConfig(); currentProvider = "anthropic"; restartCount = 0; inFallback = false; errorBuffer = ""; lastErrorTime = 0; errorCount = 0; constructor(config = {}) { this.config = { enabled: true, maxRestarts: 3, restartDelayMs: 2e3, errorPatterns: DEFAULT_ERROR_PATTERNS, ...config }; } /** * Check if text contains error patterns that should trigger fallback */ detectError(text) { for (const pattern of this.config.errorPatterns) { if (pattern.test(text)) { return { shouldFallback: true, reason: pattern.source }; } } return { shouldFallback: false, reason: "" }; } /** * Get environment variables for fallback provider */ getFallbackEnv() { const fallbackProvider = this.routerConfig.fallback?.provider || "qwen"; const providerConfig = this.routerConfig.providers[fallbackProvider]; if (!providerConfig) { console.error(`[fallback] Provider not configured: ${fallbackProvider}`); return {}; } return buildModelEnv(providerConfig); } /** * Check if fallback is available (has API key) */ isFallbackAvailable() { const fallbackProvider = this.routerConfig.fallback?.provider || "qwen"; const providerConfig = this.routerConfig.providers[fallbackProvider]; if (!providerConfig) return false; return !!process.env[providerConfig.apiKeyEnv]; } /** * Wrap a Claude process with fallback monitoring * Returns a function to spawn the process with automatic restart on failure */ wrapProcess(command, args, options = {}) { let currentProcess = null; let stopped = false; const startProcess = () => { const env = { ...process.env, ...options.env }; if (this.inFallback) { const fallbackEnv = this.getFallbackEnv(); Object.assign(env, fallbackEnv); this.currentProvider = this.routerConfig.fallback?.provider || "qwen"; } const isTTY = process.stdout.isTTY; currentProcess = spawn(command, args, { stdio: isTTY ? "inherit" : ["inherit", "pipe", "pipe"], env, cwd: options.cwd }); if (!isTTY) { currentProcess.stdout?.on("data", (data) => { const text = data.toString(); process.stdout.write(data); this.checkForErrors(text); }); currentProcess.stderr?.on("data", (data) => { const text = data.toString(); process.stderr.write(data); this.checkForErrors(text); }); } currentProcess.on("exit", (code, _signal) => { if (stopped) return; if (code !== 0 && this.shouldRestart()) { console.log( ` [fallback] Process exited with code ${code}, restarting with fallback...` ); this.activateFallback("exit_code"); setTimeout(() => { if (!stopped) { startProcess(); } }, this.config.restartDelayMs); } }); return currentProcess; }; return { start: startProcess, stop: () => { stopped = true; currentProcess?.kill(); }, isInFallback: () => this.inFallback, getCurrentProvider: () => this.currentProvider }; } /** * Check output text for errors and trigger fallback if needed */ checkForErrors(text) { this.errorBuffer += text; if (this.errorBuffer.length > 1e4) { this.errorBuffer = this.errorBuffer.slice(-5e3); } const { shouldFallback, reason } = this.detectError(text); if (shouldFallback) { const now = Date.now(); if (now - this.lastErrorTime < 1e3) { this.errorCount++; } else { this.errorCount = 1; } this.lastErrorTime = now; if (this.errorCount >= 2 && !this.inFallback && this.isFallbackAvailable()) { console.log(` [fallback] Detected error pattern: ${reason}`); this.activateFallback(reason); } } } /** * Activate fallback mode */ activateFallback(reason) { if (this.inFallback) return; this.inFallback = true; this.restartCount++; this.currentProvider = this.routerConfig.fallback?.provider || "qwen"; console.log( `[fallback] Switching to ${this.currentProvider} (reason: ${reason})` ); if (this.config.onFallback) { this.config.onFallback(this.currentProvider, reason); } } /** * Check if we should restart */ shouldRestart() { return this.config.enabled && this.restartCount < this.config.maxRestarts && this.isFallbackAvailable(); } /** * Reset fallback state */ reset() { this.inFallback = false; this.restartCount = 0; this.errorCount = 0; this.errorBuffer = ""; this.currentProvider = "anthropic"; } /** * Get current status */ getStatus() { return { inFallback: this.inFallback, currentProvider: this.currentProvider, restartCount: this.restartCount, fallbackAvailable: this.isFallbackAvailable() }; } } function spawnWithFallback(command, args, options = {}) { const monitor = new FallbackMonitor({ onFallback: options.onFallback }); const wrapper = monitor.wrapProcess(command, args, options); return wrapper.start(); } function getEnvWithFallback() { const config = loadModelRouterConfig(); const env = {}; if (config.fallback?.enabled) { const fallbackProvider = config.providers[config.fallback.provider]; if (fallbackProvider) { env["STACKMEMORY_FALLBACK_PROVIDER"] = config.fallback.provider; env["STACKMEMORY_FALLBACK_MODEL"] = fallbackProvider.model; env["STACKMEMORY_FALLBACK_URL"] = fallbackProvider.baseUrl || ""; env["STACKMEMORY_FALLBACK_KEY_ENV"] = fallbackProvider.apiKeyEnv; } } return env; } export { FallbackMonitor, getEnvWithFallback, spawnWithFallback }; //# sourceMappingURL=fallback-monitor.js.map