UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

139 lines (138 loc) • 5.78 kB
import { readFile } from "node:fs/promises"; import { homedir } from "node:os"; import { join } from "node:path"; import { AsyncMutex } from "../utils/asyncMutex.js"; import { logger } from "../utils/logger.js"; import { ACCOUNT_COOLING_REASONS, MAX_COOLDOWN_MS_BY_REASON, } from "./routingEvidence.js"; import { writeJsonSnapshotAtomically } from "./snapshotPersistence.js"; const COOLDOWN_FILE = "account-cooldowns.json"; const VALID_REASONS = new Set(ACCOUNT_COOLING_REASONS); let customCooldownFilePath = null; let cacheLoaded = false; let cacheLoadPromise = null; let memoryCache = {}; const mutationMutex = new AsyncMutex(); export function initAccountCooldown(cooldownFilePath) { customCooldownFilePath = cooldownFilePath; cacheLoaded = false; cacheLoadPromise = null; memoryCache = {}; } function getCooldownFilePath() { return customCooldownFilePath ?? join(homedir(), ".neurolink", COOLDOWN_FILE); } function isPersistedCooldown(value) { if (!value || typeof value !== "object") { return false; } const candidate = value; return (typeof candidate.coolingUntil === "number" && Number.isFinite(candidate.coolingUntil) && typeof candidate.updatedAt === "number" && Number.isFinite(candidate.updatedAt) && typeof candidate.reason === "string" && VALID_REASONS.has(candidate.reason)); } /** * Cap a persisted cooldown at what its reason can plausibly mean, measured from * when it was written. * * Entries written before per-reason ceilings existed can hold a wildly * out-of-range wait — a "session" cooldown running for days, from a single stale * reset timestamp. Clamping on load heals those without operator action. * Clamping rather than dropping keeps a legitimate long weekly cooldown intact. */ function sanitizePersistedCooldown(accountKey, entry) { const ceiling = MAX_COOLDOWN_MS_BY_REASON[entry.reason]; if (ceiling === undefined) { return entry; } const latest = entry.updatedAt + ceiling; if (entry.coolingUntil <= latest) { return entry; } // Announce it: an account silently parked far beyond what its reason can mean // is exactly the condition that is hard to diagnose from the outside, and this // runs once per process so it cannot become noise. const hours = (ms) => (ms / 3_600_000).toFixed(1); logger.always(`[proxy] cooldown clamp: ${accountKey} ${entry.reason} entry healed from ` + `${hours(entry.coolingUntil - entry.updatedAt)}h to ` + `${hours(ceiling)}h — the stored wait exceeded what "${entry.reason}" can mean`); return { ...entry, coolingUntil: latest }; } /** * Read the cooldown file as it is now. A missing file is empty; any other * failure (unreadable, corrupt) returns `fallback`, so a write never discards * entries just because one read failed. */ async function readCooldownsFromDisk(fallback) { try { const parsed = JSON.parse(await readFile(getCooldownFilePath(), "utf8")); return Object.fromEntries(Object.entries(parsed) .filter((entry) => isPersistedCooldown(entry[1])) .map(([key, entry]) => [key, sanitizePersistedCooldown(key, entry)])); } catch (error) { return error.code === "ENOENT" ? {} : fallback; } } async function ensureAccountCooldownsLoaded() { if (!cacheLoaded) { if (!cacheLoadPromise) { cacheLoadPromise = (async () => { memoryCache = await readCooldownsFromDisk({}); cacheLoaded = true; })().finally(() => { cacheLoadPromise = null; }); } await cacheLoadPromise; } } export async function loadAccountCooldowns() { await ensureAccountCooldownsLoaded(); return mutationMutex.runExclusive(async () => ({ ...memoryCache })); } export async function saveAccountCooldown(accountKey, coolingUntil, reason) { await mutationMutex.runExclusive(async () => { await ensureAccountCooldownsLoaded(); // Another worker (a draining one during a rolling restart) may have written // the file since this one loaded it, so the change folds onto the file as // it is now rather than onto this worker's copy. Best-effort: the mutex is // per process, so two workers writing within the same few ms can still // lose one update. const onDisk = await readCooldownsFromDisk(memoryCache); memoryCache = onDisk; const current = onDisk[accountKey]; if (current && current.coolingUntil > coolingUntil) { return; } memoryCache = { ...onDisk, [accountKey]: { coolingUntil, reason, updatedAt: Date.now() }, }; await writeJsonSnapshotAtomically(getCooldownFilePath(), memoryCache); }); } export async function clearAccountCooldown(accountKey, expectedCoolingUntil) { await mutationMutex.runExclusive(async () => { await ensureAccountCooldownsLoaded(); // Compare against the file, not this worker's copy: another worker may // have extended this cooldown since, and that one must survive. Same // best-effort limit as saveAccountCooldown: no cross-process lock. const onDisk = await readCooldownsFromDisk(memoryCache); memoryCache = onDisk; const current = onDisk[accountKey]; if (!current) { return; } if (expectedCoolingUntil !== undefined && current.coolingUntil !== expectedCoolingUntil) { return; } const next = { ...onDisk }; delete next[accountKey]; memoryCache = next; await writeJsonSnapshotAtomically(getCooldownFilePath(), memoryCache); }); }