@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
JavaScript
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);
});
}