Skip to content

Commit 5f4fc75

Browse files
committed
fix(runtime): centralize timeout grace clamping
1 parent 530351e commit 5f4fc75

10 files changed

Lines changed: 69 additions & 9 deletions

File tree

extensions/codex/src/app-server/attempt-timeouts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
1+
import { addTimerTimeoutGraceMs, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
22

33
export const CODEX_APP_SERVER_STARTUP_TIMEOUT_FLOOR_MS = 100;
44
export const CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS = 60_000;
@@ -108,5 +108,5 @@ export function resolveCodexTurnTerminalIdleTimeoutMs(value: number | undefined)
108108
export function resolveCodexGatewayTimeoutWithGraceMs(timeoutMs: number, graceMs = 10_000): number {
109109
const timeout = resolvePositiveIntegerTimeoutMs(timeoutMs, 1);
110110
const grace = resolveTimerTimeoutMs(graceMs, 0, 0);
111-
return resolveTimerTimeoutMs(timeout + grace, timeout);
111+
return addTimerTimeoutGraceMs(timeout, grace) ?? timeout;
112112
}

extensions/google-meet/src/transports/chrome-browser-proxy.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
12
import type { PluginRuntime } from "openclaw/plugin-sdk/plugin-runtime";
23
import { describe, expect, it, vi } from "vitest";
34
import { callBrowserProxyOnNode } from "./chrome-browser-proxy.js";
@@ -36,4 +37,30 @@ describe("Google Meet Chrome browser proxy", () => {
3637
timeoutMs: 5_100,
3738
});
3839
});
40+
41+
it("caps oversized node proxy gateway timeouts", async () => {
42+
const invoke = vi.fn(async () => ({
43+
ok: true,
44+
payloadJSON: JSON.stringify({ result: { ok: true } }),
45+
}));
46+
const runtime = {
47+
nodes: {
48+
invoke,
49+
},
50+
} as unknown as PluginRuntime;
51+
52+
await callBrowserProxyOnNode({
53+
runtime,
54+
nodeId: "node-1",
55+
method: "GET",
56+
path: "/tabs",
57+
timeoutMs: Number.MAX_SAFE_INTEGER,
58+
});
59+
60+
expect(invoke).toHaveBeenCalledWith(
61+
expect.objectContaining({
62+
timeoutMs: MAX_TIMER_TIMEOUT_MS,
63+
}),
64+
);
65+
});
3966
});

extensions/google-meet/src/transports/chrome-browser-proxy.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { addTimerTimeoutGraceMs } from "openclaw/plugin-sdk/number-runtime";
12
import type { PluginRuntime } from "openclaw/plugin-sdk/plugin-runtime";
23

34
type BrowserProxyResult = {
@@ -189,7 +190,7 @@ export async function callBrowserProxyOnNode(params: {
189190
body: params.body,
190191
timeoutMs: params.timeoutMs,
191192
},
192-
timeoutMs: params.timeoutMs + 5_000,
193+
timeoutMs: addTimerTimeoutGraceMs(params.timeoutMs) ?? 1,
193194
});
194195
return parseBrowserProxyResult(raw);
195196
}

extensions/google-meet/src/transports/chrome.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
12
import { describe, expect, it } from "vitest";
23
import { testing } from "./chrome.js";
34

@@ -9,4 +10,11 @@ describe("google meet chrome transport", () => {
910
}),
1011
).toThrow("Google Meet browser status JSON is malformed.");
1112
});
13+
14+
it("caps browser gateway timeout padding", () => {
15+
expect(testing.resolveBrowserGatewayTimeoutMsForTest(10_000)).toBe(15_000);
16+
expect(testing.resolveBrowserGatewayTimeoutMsForTest(Number.MAX_SAFE_INTEGER)).toBe(
17+
MAX_TIMER_TIMEOUT_MS,
18+
);
19+
});
1220
});

extensions/google-meet/src/transports/chrome.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
22
import { callGatewayFromCli } from "openclaw/plugin-sdk/gateway-runtime";
3+
import { addTimerTimeoutGraceMs } from "openclaw/plugin-sdk/number-runtime";
34
import type { PluginRuntime } from "openclaw/plugin-sdk/plugin-runtime";
45
import type { RuntimeLogger } from "openclaw/plugin-sdk/plugin-runtime";
56
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -50,6 +51,7 @@ export const testing = {
5051
},
5152
meetStatusScriptForTest: meetStatusScript,
5253
parseMeetBrowserStatusForTest: parseMeetBrowserStatus,
54+
resolveBrowserGatewayTimeoutMsForTest: resolveBrowserGatewayTimeoutMs,
5355
};
5456

5557
function isGoogleMeetTalkBackMode(mode: GoogleMeetMode): boolean {
@@ -291,7 +293,7 @@ async function callLocalBrowserRequest(params: BrowserRequestParams) {
291293
"browser.request",
292294
{
293295
json: true,
294-
timeout: String(params.timeoutMs + 5_000),
296+
timeout: String(resolveBrowserGatewayTimeoutMs(params.timeoutMs)),
295297
},
296298
{
297299
method: params.method,
@@ -303,6 +305,10 @@ async function callLocalBrowserRequest(params: BrowserRequestParams) {
303305
);
304306
}
305307

308+
function resolveBrowserGatewayTimeoutMs(timeoutMs: number): number {
309+
return addTimerTimeoutGraceMs(timeoutMs) ?? 1;
310+
}
311+
306312
function mergeBrowserNotes(
307313
browser: GoogleMeetChromeHealth | undefined,
308314
notes: string[],
@@ -1015,7 +1021,7 @@ export async function launchChromeMeetOnNode(params: {
10151021
audioBridgeCommand: params.config.chrome.audioBridgeCommand,
10161022
audioBridgeHealthCommand: params.config.chrome.audioBridgeHealthCommand,
10171023
},
1018-
timeoutMs: params.config.chrome.joinTimeoutMs + 5_000,
1024+
timeoutMs: addTimerTimeoutGraceMs(params.config.chrome.joinTimeoutMs) ?? 1,
10191025
});
10201026
const result = parseNodeStartResult(raw);
10211027
if (result.audioBridge?.type === "node-command-pair") {

extensions/qa-lab/src/timer-timeouts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
clampTimerTimeoutMs,
2+
addTimerTimeoutGraceMs,
33
clampPositiveTimerTimeoutMs,
44
MAX_TIMER_TIMEOUT_MS,
55
resolveTimerTimeoutMs,
@@ -17,5 +17,5 @@ export function resolveQaGatewayTimeoutWithGraceMs(
1717
return MAX_TIMER_TIMEOUT_MS;
1818
}
1919
const grace = resolveTimerTimeoutMs(graceMs, 0, 0);
20-
return clampTimerTimeoutMs(timeout + grace);
20+
return addTimerTimeoutGraceMs(timeout, grace);
2121
}

src/plugin-sdk/number-runtime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export {
66
parseFiniteNumber,
77
clampTimerTimeoutMs,
88
clampPositiveTimerTimeoutMs,
9+
addTimerTimeoutGraceMs,
910
resolvePositiveTimerTimeoutMs,
1011
resolveTimerTimeoutMs,
1112
finiteSecondsToTimerSafeMilliseconds,

src/plugins/cli-gateway-nodes-runtime.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import {
44
GATEWAY_CLIENT_NAMES,
55
} from "../../packages/gateway-protocol/src/client-info.js";
66
import { callGateway } from "../gateway/call.js";
7-
import { clampTimerTimeoutMs } from "../shared/number-coercion.js";
7+
import { addTimerTimeoutGraceMs } from "../shared/number-coercion.js";
88
import type { PluginRuntime } from "./runtime/types.js";
99

1010
export function resolvePluginCliNodeInvokeGatewayTimeoutMs(
1111
timeoutMs: number | undefined,
1212
): number | undefined {
1313
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0
14-
? clampTimerTimeoutMs(timeoutMs + 5_000)
14+
? addTimerTimeoutGraceMs(timeoutMs)
1515
: undefined;
1616
}
1717

src/shared/number-coercion.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
asFiniteNumber,
44
asFiniteNumberInRange,
55
asSafeIntegerInRange,
6+
addTimerTimeoutGraceMs,
67
clampPositiveTimerTimeoutMs,
78
clampTimerTimeoutMs,
89
finiteSecondsToTimerSafeMilliseconds,
@@ -100,6 +101,12 @@ describe("number-coercion", () => {
100101
expect(resolveTimerTimeoutMs(Number.NaN, 0, 0)).toBe(0);
101102
expect(resolveTimerTimeoutMs(Number.NaN, Number.POSITIVE_INFINITY, 25)).toBe(25);
102103
expect(resolveTimerTimeoutMs(Number.MAX_SAFE_INTEGER, 5000)).toBe(MAX_TIMER_TIMEOUT_MS);
104+
expect(addTimerTimeoutGraceMs(10_000)).toBe(15_000);
105+
expect(addTimerTimeoutGraceMs(10_000, 500)).toBe(10_500);
106+
expect(addTimerTimeoutGraceMs(MAX_TIMER_TIMEOUT_MS - 100, 500)).toBe(MAX_TIMER_TIMEOUT_MS);
107+
expect(addTimerTimeoutGraceMs(Number.MAX_SAFE_INTEGER)).toBe(MAX_TIMER_TIMEOUT_MS);
108+
expect(addTimerTimeoutGraceMs(Number.MAX_VALUE)).toBe(MAX_TIMER_TIMEOUT_MS);
109+
expect(addTimerTimeoutGraceMs(Number.NaN)).toBeUndefined();
103110
});
104111

105112
test("seconds helpers reject unsafe millisecond values", () => {

src/shared/number-coercion.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,16 @@ export function resolveTimerTimeoutMs(valueMs: unknown, fallbackMs: number, minM
126126
return Math.min(Math.max(Math.floor(value), min), MAX_TIMER_TIMEOUT_MS);
127127
}
128128

129+
export function addTimerTimeoutGraceMs(timeoutMs: unknown, graceMs = 5_000): number | undefined {
130+
const timeout = asFiniteNumber(timeoutMs);
131+
const grace = asFiniteNumber(graceMs);
132+
if (timeout === undefined || grace === undefined) {
133+
return undefined;
134+
}
135+
const withGrace = timeout + grace;
136+
return Number.isFinite(withGrace) ? clampTimerTimeoutMs(withGrace) : MAX_TIMER_TIMEOUT_MS;
137+
}
138+
129139
export function finiteSecondsToTimerSafeMilliseconds(
130140
value: unknown,
131141
opts: { floorSeconds?: boolean } = {},

0 commit comments

Comments
 (0)