Skip to content

Commit bbc4254

Browse files
committed
fix(agents): bound probe throttle cache
1 parent 275c929 commit bbc4254

File tree

2 files changed

+70
-1
lines changed

2 files changed

+70
-1
lines changed

src/agents/model-fallback.probe.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,36 @@ describe("runWithModelFallback – probe logic", () => {
251251
expectPrimaryProbeSuccess(result, run, "probed-ok");
252252
});
253253

254+
it("prunes stale probe throttle entries before checking eligibility", () => {
255+
_probeThrottleInternals.lastProbeAttempt.set(
256+
"stale",
257+
NOW - _probeThrottleInternals.PROBE_STATE_TTL_MS - 1,
258+
);
259+
_probeThrottleInternals.lastProbeAttempt.set("fresh", NOW - 5_000);
260+
261+
expect(_probeThrottleInternals.lastProbeAttempt.has("stale")).toBe(true);
262+
263+
expect(_probeThrottleInternals.isProbeThrottleOpen(NOW, "fresh")).toBe(false);
264+
265+
expect(_probeThrottleInternals.lastProbeAttempt.has("stale")).toBe(false);
266+
expect(_probeThrottleInternals.lastProbeAttempt.has("fresh")).toBe(true);
267+
});
268+
269+
it("caps probe throttle state by evicting the oldest entries", () => {
270+
for (let i = 0; i < _probeThrottleInternals.MAX_PROBE_KEYS; i += 1) {
271+
_probeThrottleInternals.lastProbeAttempt.set(`key-${i}`, NOW - (i + 1));
272+
}
273+
274+
_probeThrottleInternals.markProbeAttempt(NOW, "freshest");
275+
276+
expect(_probeThrottleInternals.lastProbeAttempt.size).toBe(
277+
_probeThrottleInternals.MAX_PROBE_KEYS,
278+
);
279+
expect(_probeThrottleInternals.lastProbeAttempt.has("freshest")).toBe(true);
280+
expect(_probeThrottleInternals.lastProbeAttempt.has("key-255")).toBe(false);
281+
expect(_probeThrottleInternals.lastProbeAttempt.has("key-0")).toBe(true);
282+
});
283+
254284
it("handles non-finite soonest safely (treats as probe-worthy)", async () => {
255285
const cfg = makeCfg();
256286

src/agents/model-fallback.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,17 +342,51 @@ const lastProbeAttempt = new Map<string, number>();
342342
const MIN_PROBE_INTERVAL_MS = 30_000; // 30 seconds between probes per key
343343
const PROBE_MARGIN_MS = 2 * 60 * 1000;
344344
const PROBE_SCOPE_DELIMITER = "::";
345+
const PROBE_STATE_TTL_MS = 24 * 60 * 60 * 1000;
346+
const MAX_PROBE_KEYS = 256;
345347

346348
function resolveProbeThrottleKey(provider: string, agentDir?: string): string {
347349
const scope = String(agentDir ?? "").trim();
348350
return scope ? `${scope}${PROBE_SCOPE_DELIMITER}${provider}` : provider;
349351
}
350352

353+
function pruneProbeState(now: number): void {
354+
for (const [key, ts] of lastProbeAttempt) {
355+
if (!Number.isFinite(ts) || ts <= 0 || now - ts > PROBE_STATE_TTL_MS) {
356+
lastProbeAttempt.delete(key);
357+
}
358+
}
359+
}
360+
361+
function enforceProbeStateCap(): void {
362+
while (lastProbeAttempt.size > MAX_PROBE_KEYS) {
363+
let oldestKey: string | null = null;
364+
let oldestTs = Number.POSITIVE_INFINITY;
365+
for (const [key, ts] of lastProbeAttempt) {
366+
if (ts < oldestTs) {
367+
oldestKey = key;
368+
oldestTs = ts;
369+
}
370+
}
371+
if (!oldestKey) {
372+
break;
373+
}
374+
lastProbeAttempt.delete(oldestKey);
375+
}
376+
}
377+
351378
function isProbeThrottleOpen(now: number, throttleKey: string): boolean {
379+
pruneProbeState(now);
352380
const lastProbe = lastProbeAttempt.get(throttleKey) ?? 0;
353381
return now - lastProbe >= MIN_PROBE_INTERVAL_MS;
354382
}
355383

384+
function markProbeAttempt(now: number, throttleKey: string): void {
385+
pruneProbeState(now);
386+
lastProbeAttempt.set(throttleKey, now);
387+
enforceProbeStateCap();
388+
}
389+
356390
function shouldProbePrimaryDuringCooldown(params: {
357391
isPrimary: boolean;
358392
hasFallbackCandidates: boolean;
@@ -383,7 +417,12 @@ export const _probeThrottleInternals = {
383417
lastProbeAttempt,
384418
MIN_PROBE_INTERVAL_MS,
385419
PROBE_MARGIN_MS,
420+
PROBE_STATE_TTL_MS,
421+
MAX_PROBE_KEYS,
386422
resolveProbeThrottleKey,
423+
isProbeThrottleOpen,
424+
pruneProbeState,
425+
markProbeAttempt,
387426
} as const;
388427

389428
type CooldownDecision =
@@ -536,7 +575,7 @@ export async function runWithModelFallback<T>(params: {
536575
}
537576

538577
if (decision.markProbe) {
539-
lastProbeAttempt.set(probeThrottleKey, now);
578+
markProbeAttempt(now, probeThrottleKey);
540579
}
541580
if (
542581
decision.reason === "rate_limit" ||

0 commit comments

Comments
 (0)