Skip to content

Commit 9a8fd3d

Browse files
Cleo ThornsburgCleo Thornsburg
authored andcommitted
fix(cron): avoid caching deadline-clamped probes
1 parent 0d27453 commit 9a8fd3d

2 files changed

Lines changed: 48 additions & 3 deletions

File tree

src/cron/isolated-agent/model-preflight.runtime.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,44 @@ describe("preflightCronModelProvider", () => {
270270
expect(fetchWithSsrFGuardMock).toHaveBeenCalledTimes(2);
271271
});
272272

273+
it("does not cache a failure from a deadline-clamped probe", async () => {
274+
vi.useFakeTimers();
275+
vi.setSystemTime(1_000);
276+
fetchWithSsrFGuardMock.mockRejectedValueOnce(new DOMException("timed out", "TimeoutError"));
277+
278+
const cfg = {
279+
models: {
280+
providers: {
281+
ollama: {
282+
api: "ollama" as const,
283+
baseUrl: "http://127.0.0.1:11434",
284+
models: [],
285+
},
286+
},
287+
},
288+
};
289+
290+
const first = await preflightCronModelProvider({
291+
cfg,
292+
provider: "ollama",
293+
model: "qwen3:14b",
294+
deadlineMs: 1_200,
295+
});
296+
297+
expect(first.status).toBe("unavailable");
298+
expect(requireFetchPreflightRequest().timeoutMs).toBe(200);
299+
300+
mockReachableResponse(200);
301+
const nextRun = await preflightCronModelProvider({
302+
cfg,
303+
provider: "ollama",
304+
model: "qwen3:14b",
305+
});
306+
307+
expect(nextRun).toEqual({ status: "available" });
308+
expect(fetchWithSsrFGuardMock).toHaveBeenCalledTimes(2);
309+
});
310+
273311
it("retries an unavailable endpoint after the cache ttl", async () => {
274312
fetchWithSsrFGuardMock.mockRejectedValueOnce(new Error("ECONNREFUSED")).mockResolvedValueOnce({
275313
response: { status: 200 },

src/cron/isolated-agent/model-preflight.runtime.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ export async function preflightCronModelProvider(params: {
263263
let lastError: unknown;
264264
let attempts = 0;
265265
let budgetExhausted = false;
266+
let cacheableFailure = true;
266267
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
267268
const remainingBudgetMs = resolveRemainingBudgetMs(params.deadlineMs);
268269
if (remainingBudgetMs !== undefined && remainingBudgetMs <= 0) {
@@ -271,18 +272,24 @@ export async function preflightCronModelProvider(params: {
271272
break;
272273
}
273274
attempts = attempt;
275+
const probeTimeoutMs =
276+
remainingBudgetMs === undefined ? timeoutMs : Math.min(timeoutMs, remainingBudgetMs);
274277
try {
275278
await probeLocalProviderEndpoint({
276279
api,
277280
baseUrl,
278-
timeoutMs:
279-
remainingBudgetMs === undefined ? timeoutMs : Math.min(timeoutMs, remainingBudgetMs),
281+
timeoutMs: probeTimeoutMs,
280282
});
281283
const result: EndpointPreflightResult = { status: "available" };
282284
preflightCache.set(cacheKey, { checkedAtMs: nowMs, result });
283285
return { status: "available" };
284286
} catch (error) {
285287
lastError = error;
288+
// A deadline-clamped probe did not receive the configured health-check
289+
// window, so its failure must not poison the endpoint cache.
290+
if (probeTimeoutMs < timeoutMs) {
291+
cacheableFailure = false;
292+
}
286293
if (attempt < maxAttempts) {
287294
const remainingDelayBudgetMs = resolveRemainingBudgetMs(params.deadlineMs);
288295
if (remainingDelayBudgetMs !== undefined && remainingDelayBudgetMs <= 0) {
@@ -305,7 +312,7 @@ export async function preflightCronModelProvider(params: {
305312
error: lastError ?? new Error("cron model preflight chain budget exhausted"),
306313
attempts,
307314
};
308-
if (!budgetExhausted) {
315+
if (!budgetExhausted && cacheableFailure) {
309316
preflightCache.set(cacheKey, { checkedAtMs: nowMs, result });
310317
}
311318
return buildUnavailableResult({

0 commit comments

Comments
 (0)