[GH-ISSUE #3392] [Bug] Accepted long browser scenario waits and deadlines overflow into millisecond timers #1259

Closed
opened 2026-10-16 21:03:29 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 14, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3392

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Process capture or runtime observation

Observed behavior

The public scenario schema accepts duration_ms and timeout_ms values of 2,147,483,648, but the Playwright action path passes them to timers that overflow on the supported Node runtime. Through capture-browser-scenario, a requested wait completes in milliseconds and reports success. A wait for an already-visible button instead reports that its 2,147,483,648 ms deadline expired after only a few milliseconds. Short-duration controls behave normally.

The long wait completed successfully in 3 ms. The long-deadline action failed in 6 ms with locator.waitFor: Timeout 2147483648ms exceeded. even though the button was present and visible. Its warning reported a computed remaining interval of 2,147,483,647.907 ms and said the timeout was set to 1. Changing that action's deadline to 1,000 ms completed successfully in 89 ms. Each completed CLI invocation returned confirmed owned-browser cleanup.

Running the exact inline reproduction again produced a 3 ms completed long wait, a 5 ms failed long deadline, a 102 ms completed 100 ms wait, and an 82 ms completed 1,000 ms visible-button action. Both oversized timer cases emitted the overflow warning on both executed attempts.

Reproduction

  1. Start from a clean source checkout at the specified baseline, install its locked dependencies, and build the runtime:
git clone https://github.com/morluto/rea.git rea-repro
cd rea-repro
git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063
npm ci
npm run build:cached
  1. Set CHROME to an already-installed Chrome-family executable's absolute path, for example with export CHROME='<absolute-path-to-chrome>'. From the repository root, run the following script. It creates a local page with a visible button and every scenario input file, invokes only the public capture CLI, prints the final action and any timer warning, and closes its server.
: "${CHROME:?Set CHROME to an installed Chrome-family executable}"
case_dir="$(mktemp -d)"
mkdir -p "$case_dir/home" "$case_dir/temp"
cat > "$case_dir/repro.cjs" <<'JS'
const { createServer } = require('node:http');
const { execFile } = require('node:child_process');
const { promisify } = require('node:util');
const { writeFile } = require('node:fs/promises');
const { join } = require('node:path');
const run = promisify(execFile);
(async () => {
  const server = createServer((request, response) => {
    response.setHeader('Content-Type', 'text/html');
    response.end('<!doctype html><button id="run">Run</button>');
  });
  await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
  try {
    const url = `http://127.0.0.1:${server.address().port}/`;
    const cases = [
      ['long-wait', { action: 'wait_for_timeout', duration_ms: 2147483648 }],
      ['short-wait', { action: 'wait_for_timeout', duration_ms: 100 }],
      ['long-deadline', {
        action: 'wait_for', locator: { kind: 'css', selector: '#run' },
        state: 'visible', timeout_ms: 2147483648
      }],
      ['short-deadline', {
        action: 'wait_for', locator: { kind: 'css', selector: '#run' },
        state: 'visible', timeout_ms: 1000
      }]
    ];
    for (const [name, action] of cases) {
      const input = join(__dirname, `${name}.json`);
      await writeFile(input, JSON.stringify({
        browser: { mode: 'launch', executable_path: process.env.CHROME },
        start_url: { url }, actions: [{ step_id: 'observe', ...action }]
      }));
      const result = await run(process.execPath,
        ['scripts/rea.mjs', 'capture-browser-scenario', input, '--json'],
        { cwd: process.cwd(), env: process.env, timeout: 120000, maxBuffer: 1048576 });
      const capture = JSON.parse(result.stdout).normalized_result;
      const step = capture.steps.at(-1);
      console.log(JSON.stringify({
        name, status: step.status, elapsed_ms: step.elapsed_ms,
        error: step.error, cleanup: capture.browser.cleanup
      }));
      if (result.stderr) console.log(result.stderr);
    }
  } finally {
    await new Promise(resolve => server.close(resolve));
  }
})().catch(error => { console.error(error); process.exitCode = 1; });
JS
env HOME="$case_dir/home" TMPDIR="$case_dir/temp" node "$case_dir/repro.cjs"
rm -rf "$case_dir"
  1. Compare the two oversized cases with the 100 ms wait and 1,000 ms visible-button controls. The script's 120-second child-process watchdog bounds the reproduction if long waits start working. A watchdog termination after that interval is not evidence that the entire requested wait elapsed.

Expected behavior

An accepted wait_for_timeout action must not complete before its requested duration unless cancelled. An accepted action deadline must not expire before its requested interval. The already-visible button should complete the wait_for action within either admitted deadline. If the provider cannot represent a duration, validation should return an actionable range error before browser execution rather than silently shortening it.

src/domain/browserScenarioValues.ts:159 describes timeout_ms as an optional action deadline and accepts positive integers. src/domain/browserScenarioValues.ts:214 describes duration_ms as the requested wait, ending early on cancellation, and also accepts positive integers. The CLI uses this shared schema.

Impact and scope

P2, supported by the reproduced results. Accepted long-duration inputs either falsely report a completed wait or fail an immediately satisfiable action with a misleading deadline-expired result. The demonstrated threshold is just above the signed 32-bit millisecond timer boundary; ordinary short deadlines are unaffected in the controls.

A workaround is available. A 1,000 ms deadline succeeded for the same visible-button action. Callers can keep individual timer intervals within the runtime's signed 32-bit millisecond range; longer waits can be divided into separately declared bounded waits.

REA version

6.4.0 (main at 8dde6370d0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1 arm64; locked playwright-core 1.58.2; Google Chrome 155.0.8059.40. The runtime and dependencies were already built and installed for the observed reproduction.

Target and artifact context

Local loopback HTTP page with a visible #run button, JSON browser scenario inputs, and returned browser scenario captures. The reproduction used owned headless browser profiles and local HTTP pages.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

The observed long-wait CLI invocation emitted:

TimeoutOverflowWarning: 2147483648 does not fit into a 32-bit signed integer.
Timeout duration was set to 1.

The observed long-deadline action error was locator.waitFor: Timeout 2147483648ms exceeded. The repeated execution completed the long wait in 3 ms, failed the long deadline in 5 ms, completed the 100 ms wait in 102 ms, and completed the 1,000 ms visible-button control in 82 ms.

Typed outcome or recovery expected

An oversized wait must either fail input validation before launching a browser or remain pending for the observation interval and respond to cancellation without reporting premature completion. The long wait_for deadline must either receive the same explicit range rejection or allow the visible button to complete without a timer-overflow failure. The short wait must still wait its requested interval, and the 1,000 ms button control must still succeed. Full multi-day completion was not tested and is unnecessary to distinguish the observed immediate overflow.

Analysis or suspected cause

Source-derived reasoning: src/browser/PlaywrightScenarioActions.ts:44 forwards the admitted deadline to Playwright action options, and src/browser/PlaywrightScenarioActions.ts:87 forwards the admitted wait to page.waitForTimeout. The locked dependency's node_modules/playwright-core/lib/server/progress.js:78 and node_modules/playwright-core/lib/server/progress.js:87 use ordinary Node timers for these intervals. This path is consistent with the executed public CLI actions and timer warnings above; the observed failure was locally reproduced with the complete inline reproduction.

Additional context

Related closed issue #2597 concerns cancellation during browser opening retaining cleanup and preventing provider reuse; this report concerns oversized timers during admitted browser actions with confirmed cleanup.

Related closed issue #373 defines the browser scenario contract, including timeouts; it does not report or designate this overflow behavior as intended.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3392 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Process capture or runtime observation ### Observed behavior The public scenario schema accepts `duration_ms` and `timeout_ms` values of 2,147,483,648, but the Playwright action path passes them to timers that overflow on the supported Node runtime. Through `capture-browser-scenario`, a requested wait completes in milliseconds and reports success. A wait for an already-visible button instead reports that its 2,147,483,648 ms deadline expired after only a few milliseconds. Short-duration controls behave normally. The long wait completed successfully in 3 ms. The long-deadline action failed in 6 ms with `locator.waitFor: Timeout 2147483648ms exceeded.` even though the button was present and visible. Its warning reported a computed remaining interval of 2,147,483,647.907 ms and said the timeout was set to 1. Changing that action's deadline to 1,000 ms completed successfully in 89 ms. Each completed CLI invocation returned confirmed owned-browser cleanup. Running the exact inline reproduction again produced a 3 ms completed long wait, a 5 ms failed long deadline, a 102 ms completed 100 ms wait, and an 82 ms completed 1,000 ms visible-button action. Both oversized timer cases emitted the overflow warning on both executed attempts. ### Reproduction 1. Start from a clean source checkout at the specified baseline, install its locked dependencies, and build the runtime: ```bash git clone https://github.com/morluto/rea.git rea-repro cd rea-repro git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063 npm ci npm run build:cached ``` 2. Set `CHROME` to an already-installed Chrome-family executable's absolute path, for example with `export CHROME='<absolute-path-to-chrome>'`. From the repository root, run the following script. It creates a local page with a visible button and every scenario input file, invokes only the public capture CLI, prints the final action and any timer warning, and closes its server. ```bash : "${CHROME:?Set CHROME to an installed Chrome-family executable}" case_dir="$(mktemp -d)" mkdir -p "$case_dir/home" "$case_dir/temp" cat > "$case_dir/repro.cjs" <<'JS' const { createServer } = require('node:http'); const { execFile } = require('node:child_process'); const { promisify } = require('node:util'); const { writeFile } = require('node:fs/promises'); const { join } = require('node:path'); const run = promisify(execFile); (async () => { const server = createServer((request, response) => { response.setHeader('Content-Type', 'text/html'); response.end('<!doctype html><button id="run">Run</button>'); }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); try { const url = `http://127.0.0.1:${server.address().port}/`; const cases = [ ['long-wait', { action: 'wait_for_timeout', duration_ms: 2147483648 }], ['short-wait', { action: 'wait_for_timeout', duration_ms: 100 }], ['long-deadline', { action: 'wait_for', locator: { kind: 'css', selector: '#run' }, state: 'visible', timeout_ms: 2147483648 }], ['short-deadline', { action: 'wait_for', locator: { kind: 'css', selector: '#run' }, state: 'visible', timeout_ms: 1000 }] ]; for (const [name, action] of cases) { const input = join(__dirname, `${name}.json`); await writeFile(input, JSON.stringify({ browser: { mode: 'launch', executable_path: process.env.CHROME }, start_url: { url }, actions: [{ step_id: 'observe', ...action }] })); const result = await run(process.execPath, ['scripts/rea.mjs', 'capture-browser-scenario', input, '--json'], { cwd: process.cwd(), env: process.env, timeout: 120000, maxBuffer: 1048576 }); const capture = JSON.parse(result.stdout).normalized_result; const step = capture.steps.at(-1); console.log(JSON.stringify({ name, status: step.status, elapsed_ms: step.elapsed_ms, error: step.error, cleanup: capture.browser.cleanup })); if (result.stderr) console.log(result.stderr); } } finally { await new Promise(resolve => server.close(resolve)); } })().catch(error => { console.error(error); process.exitCode = 1; }); JS env HOME="$case_dir/home" TMPDIR="$case_dir/temp" node "$case_dir/repro.cjs" rm -rf "$case_dir" ``` 3. Compare the two oversized cases with the 100 ms wait and 1,000 ms visible-button controls. The script's 120-second child-process watchdog bounds the reproduction if long waits start working. A watchdog termination after that interval is not evidence that the entire requested wait elapsed. ### Expected behavior An accepted `wait_for_timeout` action must not complete before its requested duration unless cancelled. An accepted action deadline must not expire before its requested interval. The already-visible button should complete the `wait_for` action within either admitted deadline. If the provider cannot represent a duration, validation should return an actionable range error before browser execution rather than silently shortening it. `src/domain/browserScenarioValues.ts:159` describes `timeout_ms` as an optional action deadline and accepts positive integers. `src/domain/browserScenarioValues.ts:214` describes `duration_ms` as the requested wait, ending early on cancellation, and also accepts positive integers. The CLI uses this shared schema. ### Impact and scope P2, supported by the reproduced results. Accepted long-duration inputs either falsely report a completed wait or fail an immediately satisfiable action with a misleading deadline-expired result. The demonstrated threshold is just above the signed 32-bit millisecond timer boundary; ordinary short deadlines are unaffected in the controls. A workaround is available. A 1,000 ms deadline succeeded for the same visible-button action. Callers can keep individual timer intervals within the runtime's signed 32-bit millisecond range; longer waits can be divided into separately declared bounded waits. ### REA version 6.4.0 (main at 8dde6370d0809ad04cda3342d4e6f290f48aa063) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1 arm64; locked playwright-core 1.58.2; Google Chrome 155.0.8059.40. The runtime and dependencies were already built and installed for the observed reproduction. ### Target and artifact context Local loopback HTTP page with a visible `#run` button, JSON browser scenario inputs, and returned browser scenario captures. The reproduction used owned headless browser profiles and local HTTP pages. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence The observed long-wait CLI invocation emitted: ```text TimeoutOverflowWarning: 2147483648 does not fit into a 32-bit signed integer. Timeout duration was set to 1. ``` The observed long-deadline action error was `locator.waitFor: Timeout 2147483648ms exceeded.` The repeated execution completed the long wait in 3 ms, failed the long deadline in 5 ms, completed the 100 ms wait in 102 ms, and completed the 1,000 ms visible-button control in 82 ms. ### Typed outcome or recovery expected An oversized wait must either fail input validation before launching a browser or remain pending for the observation interval and respond to cancellation without reporting premature completion. The long `wait_for` deadline must either receive the same explicit range rejection or allow the visible button to complete without a timer-overflow failure. The short wait must still wait its requested interval, and the 1,000 ms button control must still succeed. Full multi-day completion was not tested and is unnecessary to distinguish the observed immediate overflow. ### Analysis or suspected cause Source-derived reasoning: `src/browser/PlaywrightScenarioActions.ts:44` forwards the admitted deadline to Playwright action options, and `src/browser/PlaywrightScenarioActions.ts:87` forwards the admitted wait to `page.waitForTimeout`. The locked dependency's `node_modules/playwright-core/lib/server/progress.js:78` and `node_modules/playwright-core/lib/server/progress.js:87` use ordinary Node timers for these intervals. This path is consistent with the executed public CLI actions and timer warnings above; the observed failure was locally reproduced with the complete inline reproduction. ### Additional context Related closed issue [#2597](https://github.com/morluto/rea/issues/2597) concerns cancellation during browser opening retaining cleanup and preventing provider reuse; this report concerns oversized timers during admitted browser actions with confirmed cleanup. Related closed issue [#373](https://github.com/morluto/rea/issues/373) defines the browser scenario contract, including timeouts; it does not report or designate this overflow behavior as intended.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1259
No description provided.