From f2dfb67f2c5b27381bcb16b2b2dafd1106fbf713 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 28 May 2026 23:05:26 -0400 Subject: [PATCH] fix(agents): default non-finite run wait timeouts --- src/agents/run-wait.test.ts | 34 ++++++++++++++++++++++++++++++++++ src/agents/run-wait.ts | 9 +++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/agents/run-wait.test.ts b/src/agents/run-wait.test.ts index 0dacd812e23..57ae2ad1837 100644 --- a/src/agents/run-wait.test.ts +++ b/src/agents/run-wait.test.ts @@ -252,6 +252,22 @@ describe("waitForAgentRun", () => { }); }); + it("defaults non-finite wait timeouts before sending agent.wait", async () => { + callGatewayMock.mockResolvedValue({ status: "ok" }); + + const result = await waitForAgentRun({ runId: "run-nan", timeoutMs: Number.NaN }); + + expect(result).toEqual({ status: "ok" }); + expect(callGatewayMock).toHaveBeenCalledWith({ + method: "agent.wait", + params: { + runId: "run-nan", + timeoutMs: 1, + }, + timeoutMs: 2_001, + }); + }); + it("preserves timing metadata from agent.wait", async () => { callGatewayMock.mockResolvedValue({ status: "ok", @@ -457,4 +473,22 @@ describe("waitForAgentRunsToDrain", () => { expectAgentWaitRequest(requireRequestAt(requests, 0), "run-1", 1_000); expectAgentWaitRequest(requireRequestAt(requests, 1), "run-2", 1_000); }); + + it("defaults non-finite drain timeouts before computing the deadline", async () => { + callGatewayMock.mockResolvedValue({ status: "ok" }); + let activeRunIds = ["run-1"]; + + const result = await waitForAgentRunsToDrain({ + timeoutMs: Number.NaN, + getPendingRunIds: () => { + const current = activeRunIds; + activeRunIds = []; + return current; + }, + }); + + expect(result.timedOut).toBe(false); + expect(Number.isFinite(result.deadlineAtMs)).toBe(true); + expectAgentWaitRequest(requireRequestAt(gatewayWaitRequests(), 0), "run-1", 1); + }); }); diff --git a/src/agents/run-wait.ts b/src/agents/run-wait.ts index ff25619fb2d..0b41e3b2433 100644 --- a/src/agents/run-wait.ts +++ b/src/agents/run-wait.ts @@ -1,6 +1,7 @@ import { callGateway } from "../gateway/call.js"; import { formatErrorMessage } from "../infra/errors.js"; import { normalizeBlockedLivenessWaitStatus } from "../shared/agent-liveness.js"; +import { parseFiniteNumber } from "../shared/number-coercion.js"; import { AGENT_RUN_ABORTED_ERROR, isAbortedAgentStopReason } from "./run-termination.js"; import { normalizeAgentRunTimeoutPhase, @@ -19,6 +20,10 @@ let runWaitDeps: { callGateway: GatewayCaller; } = defaultRunWaitDeps; +function resolveRunWaitTimeoutMs(value: number | undefined): number { + return Math.max(1, Math.floor(parseFiniteNumber(value) ?? 1)); +} + export type AssistantReplySnapshot = { text?: string; fingerprint?: string; @@ -176,7 +181,7 @@ export async function waitForAgentRun(params: { timeoutMs: number; callGateway?: GatewayCaller; }): Promise { - const timeoutMs = Math.max(1, Math.floor(params.timeoutMs)); + const timeoutMs = resolveRunWaitTimeoutMs(params.timeoutMs); try { const wait = await (params.callGateway ?? runWaitDeps.callGateway)({ method: "agent.wait", @@ -246,7 +251,7 @@ export async function waitForAgentRunsToDrain(params: { callGateway?: GatewayCaller; }): Promise { const deadlineAtMs = - params.deadlineAtMs ?? Date.now() + Math.max(1, Math.floor(params.timeoutMs ?? 0)); + params.deadlineAtMs ?? Date.now() + resolveRunWaitTimeoutMs(params.timeoutMs); // Runs may finish and spawn more runs, so refresh until no pending IDs remain. let pendingRunIds = new Set(