UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

269 lines (268 loc) 7.33 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { logger } from "../monitoring/logger.js"; import { isRetryableError, getErrorMessage } from "./index.js"; var CircuitState = /* @__PURE__ */ ((CircuitState2) => { CircuitState2["CLOSED"] = "closed"; CircuitState2["OPEN"] = "open"; CircuitState2["HALF_OPEN"] = "half_open"; return CircuitState2; })(CircuitState || {}); function calculateBackoff(attempt, initialDelay, maxDelay, factor) { const exponentialDelay = Math.min( initialDelay * Math.pow(factor, attempt - 1), maxDelay ); const jitter = exponentialDelay * Math.random() * 0.25; return Math.floor(exponentialDelay + jitter); } async function retry(fn, options = {}) { const { maxAttempts = 3, initialDelay = 1e3, maxDelay = 3e4, backoffFactor = 2, timeout, onRetry } = options; let lastError; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { if (timeout) { return await Promise.race([ fn(), new Promise( (_, reject) => setTimeout( () => reject(new Error(`Operation timed out after ${timeout}ms`)), timeout ) ) ]); } return await fn(); } catch (error) { lastError = error; if (!isRetryableError(error) || attempt === maxAttempts) { throw error; } const delay = calculateBackoff( attempt, initialDelay, maxDelay, backoffFactor ); logger.warn(`Retry attempt ${attempt}/${maxAttempts} after ${delay}ms`, { error: getErrorMessage(error), attempt, delay }); if (onRetry) { onRetry(attempt, error); } await new Promise((resolve) => setTimeout(resolve, delay)); } } throw lastError; } class CircuitBreaker { constructor(name, options = {}) { this.name = name; this.options = { failureThreshold: options.failureThreshold ?? 5, resetTimeout: options.resetTimeout ?? 6e4, halfOpenRequests: options.halfOpenRequests ?? 3 }; } state = "closed" /* CLOSED */; failures = 0; successCount = 0; lastFailTime; options; async execute(fn) { if (this.state === "open" /* OPEN */) { const timeSinceLastFailure = this.lastFailTime ? Date.now() - this.lastFailTime.getTime() : 0; if (timeSinceLastFailure >= this.options.resetTimeout) { this.state = "half_open" /* HALF_OPEN */; this.successCount = 0; logger.info(`Circuit breaker ${this.name} entering half-open state`); } else { throw new Error( `Circuit breaker ${this.name} is OPEN. Retry after ${this.options.resetTimeout - timeSinceLastFailure}ms` ); } } try { const result = await fn(); if (this.state === "half_open" /* HALF_OPEN */) { this.successCount++; if (this.successCount >= this.options.halfOpenRequests) { this.state = "closed" /* CLOSED */; this.failures = 0; logger.info(`Circuit breaker ${this.name} is now CLOSED`); } } else { this.failures = 0; } return result; } catch (error) { this.handleFailure(error); throw error; } } handleFailure(_error) { this.failures++; this.lastFailTime = /* @__PURE__ */ new Date(); if (this.state === "half_open" /* HALF_OPEN */) { this.state = "open" /* OPEN */; logger.error( `Circuit breaker ${this.name} reopened due to failure in half-open state` ); } else if (this.state === "closed" /* CLOSED */ && this.failures >= this.options.failureThreshold) { this.state = "open" /* OPEN */; logger.error( `Circuit breaker ${this.name} opened after ${this.failures} failures` ); } } getState() { return this.state; } reset() { this.state = "closed" /* CLOSED */; this.failures = 0; this.successCount = 0; this.lastFailTime = void 0; logger.info(`Circuit breaker ${this.name} manually reset`); } } async function withFallback(primary, fallbacks, context) { const errors = []; try { return await primary(); } catch (error) { errors.push(error); logger.warn("Primary operation failed, trying fallbacks", { error: getErrorMessage(error), context }); } for (let i = 0; i < fallbacks.length; i++) { try { const result = await fallbacks[i](); logger.info(`Fallback ${i + 1} succeeded`, { context }); return result; } catch (error) { errors.push(error); if (i < fallbacks.length - 1) { logger.warn(`Fallback ${i + 1} failed, trying next`, { error: getErrorMessage(error), context }); } } } throw new Error( `All attempts failed. Errors: ${errors.map(getErrorMessage).join(", ")}` ); } class Bulkhead { constructor(name, maxConcurrent) { this.name = name; this.maxConcurrent = maxConcurrent; } running = 0; queue = []; async execute(fn) { if (this.running >= this.maxConcurrent) { await new Promise((resolve) => { this.queue.push(resolve); }); } this.running++; try { return await fn(); } finally { this.running--; const next = this.queue.shift(); if (next) { next(); } } } getStats() { return { running: this.running, queued: this.queue.length, maxConcurrent: this.maxConcurrent }; } } async function withTimeout(fn, timeoutMs, timeoutMessage) { return Promise.race([ fn(), new Promise( (_, reject) => setTimeout( () => reject( new Error( timeoutMessage ?? `Operation timed out after ${timeoutMs}ms` ) ), timeoutMs ) ) ]); } async function gracefulDegrade(fn, defaultValue, logContext) { try { return await fn(); } catch (error) { logger.warn("Operation failed, using default value", { error: getErrorMessage(error), ...logContext }); return defaultValue; } } function createResilientOperation(name, options = {}) { const circuitBreaker = options.circuitBreaker ? new CircuitBreaker(name, options.circuitBreaker) : null; const bulkhead = options.bulkhead ? new Bulkhead(name, options.bulkhead) : null; return async (fn) => { let currentFn = fn; if (bulkhead) { const wrapped = currentFn; currentFn = () => bulkhead.execute(wrapped); } if (options.timeout) { const wrapped = currentFn; currentFn = () => withTimeout(wrapped, options.timeout); } if (options.retry) { const wrapped = currentFn; currentFn = () => retry(wrapped, options.retry); } if (circuitBreaker) { const wrapped = currentFn; currentFn = () => circuitBreaker.execute(wrapped); } if (options.fallback) { return withFallback(currentFn, [options.fallback]); } return currentFn(); }; } export { Bulkhead, CircuitBreaker, CircuitState, calculateBackoff, createResilientOperation, gracefulDegrade, retry, withFallback, withTimeout };