@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
JavaScript
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