From 9ef699fedccca55a7cac71f014aabcd7399a1a6e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 13:43:21 -0400 Subject: [PATCH] fix(gateway): bound maintenance run expiry checks --- src/gateway/server-maintenance.test.ts | 46 ++++++++++++++++++++++++++ src/gateway/server-maintenance.ts | 5 +-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/gateway/server-maintenance.test.ts b/src/gateway/server-maintenance.test.ts index bd4d1911e96..8d6839f9e6a 100644 --- a/src/gateway/server-maintenance.test.ts +++ b/src/gateway/server-maintenance.test.ts @@ -394,6 +394,52 @@ describe("startGatewayMaintenanceTimers", () => { stopMaintenanceTimers(timers); }); + it("evicts pending accepted agent dedupe entries with invalid run expiry", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-03-22T00:00:00Z")); + const { startGatewayMaintenanceTimers } = await import("./server-maintenance.js"); + const deps = createMaintenanceTimerDeps(); + const now = Date.now(); + deps.dedupe.set("agent:invalid-expiry-pending-agent", { + ts: now - DEDUPE_TTL_MS - 1, + ok: true, + payload: { + runId: "invalid-expiry-pending-agent", + sessionKey: "agent:main:main", + status: "accepted", + expiresAtMs: Number.POSITIVE_INFINITY, + }, + }); + + const timers = startGatewayMaintenanceTimers(deps); + + await vi.advanceTimersByTimeAsync(60_000); + + expect(deps.dedupe.has("agent:invalid-expiry-pending-agent")).toBe(false); + + stopMaintenanceTimers(timers); + }); + + it("aborts active runs with invalid expiry timestamps", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-03-22T00:00:00Z")); + const { startGatewayMaintenanceTimers } = await import("./server-maintenance.js"); + const deps = createMaintenanceTimerDeps(); + const runId = "run-invalid-expiry"; + const activeRun = createActiveRun("main"); + activeRun.expiresAtMs = Number.POSITIVE_INFINITY; + deps.chatAbortControllers.set(runId, activeRun); + + const timers = startGatewayMaintenanceTimers(deps); + + await vi.advanceTimersByTimeAsync(60_000); + + expect(activeRun.controller.signal.aborted).toBe(true); + expect(deps.chatAbortControllers.has(runId)).toBe(false); + + stopMaintenanceTimers(timers); + }); + it("keeps active exec approval dedupe aliases past the normal ttl", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-03-22T00:00:00Z")); diff --git a/src/gateway/server-maintenance.ts b/src/gateway/server-maintenance.ts index 935f8a9dc8f..9a68c38adf1 100644 --- a/src/gateway/server-maintenance.ts +++ b/src/gateway/server-maintenance.ts @@ -1,6 +1,7 @@ import type { HealthSummary } from "../commands/health.js"; import { sweepStaleRunContexts } from "../infra/agent-events.js"; import { cleanOldMedia } from "../media/store.js"; +import { isFutureDateTimestampMs } from "../shared/number-coercion.js"; import { abortChatRunById, type ChatAbortControllerEntry } from "./chat-abort.js"; import { pruneStaleControlPlaneBuckets } from "./control-plane-rate-limit.js"; import type { ChatRunState } from "./server-chat-state.js"; @@ -124,7 +125,7 @@ export function startGatewayMaintenanceTimers(params: { return false; } const expiresAtMs = (payload as { expiresAtMs?: unknown }).expiresAtMs; - return typeof expiresAtMs === "number" && Number.isFinite(expiresAtMs) && expiresAtMs > now; + return isFutureDateTimestampMs(expiresAtMs, { nowMs: now }); }; const isActiveRunDedupeKey = (key: string, dedupeEntry: DedupeEntry) => { if (!key.startsWith("agent:") && !key.startsWith("chat:")) { @@ -183,7 +184,7 @@ export function startGatewayMaintenanceTimers(params: { }; for (const [runId, entry] of params.chatAbortControllers) { - if (now <= entry.expiresAtMs) { + if (isFutureDateTimestampMs(entry.expiresAtMs, { nowMs: now })) { continue; } abortChatRunById(