[GH-ISSUE #7402] [Bug] Omitting command_timeout_ms leaves initial browser-scenario navigation without its default deadline #4409

Closed
opened 2026-10-31 00:49:34 +00:00 by bsakaguchi · 0 comments
Owner

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

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

A valid browser scenario that omits command_timeout_ms keeps waiting for its initial page load beyond the promised 20000 ms default. In an independent real-browser run, a loopback page containing an image whose response never ended remained in startup until the probe cancelled it at 26 seconds. The operation returned cancelled after 26189 ms with no captured steps. With the same scenario and command_timeout_ms: 2000, it instead returned provider_timeout after 2512 ms.

I reproduced this through the shared application service and its production Playwright provider with no mocks. The CLI and MCP entrypoints' use of that same path is source-confirmed; I did not independently execute the packaged CLI or an MCP request.

Reproduction

Use a fresh clone at the revision below, Node.js 24.22.1, locked dependencies installed with npm ci, and an already installed Chrome-family browser. Save this independent probe as src/browser/verify-probe.test.ts. Replace its browser executable with the installed browser's absolute path if needed; the path shown is the one exercised on macOS. The fixture serves only loopback HTTP and intentionally leaves the image response unfinished.

import { createServer } from "node:http";
import type { Socket } from "node:net";
import { expect, it } from "vitest";
import { browserScenarioSchema } from "../domain/browserScenario.js";
import { createBrowserScenarioProvider } from "../composition/browserScenario.js";
import { captureBrowserScenario } from "../application/BrowserScenarioCaptureService.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";

it("checks initial navigation with omitted and explicit deadlines against a real local browser", async () => {
  const sockets = new Set<Socket>();
  let neverRequests = 0;
  const server = createServer((request, response) => {
    if (request.url === "/never") {
      neverRequests++;
      response.writeHead(200, { "content-type": "image/png", "content-length": "100000" });
      response.flushHeaders();
    } else {
      response.writeHead(200, { "content-type": "text/html" });
      response.end('<!doctype html><img src="/never">');
    }
  });
  server.on("connection", socket => {
    sockets.add(socket);
    socket.on("close", () => sockets.delete(socket));
  });
  await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve));
  const address = server.address();
  if (address === null || typeof address === "string") throw new Error("missing listener");
  try {
    for (const timeout of [undefined, 2000]) {
      const scenario = browserScenarioSchema.parse({
        browser: { mode: "launch", executable_path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" },
        start_url: { url: `http://127.0.0.1:${address.port}/hang` },
        actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }],
        ...(timeout === undefined ? {} : { command_timeout_ms: timeout }),
      });
      const provider = createBrowserScenarioProvider(process.env);
      const controller = new AbortController();
      const abortTimer = setTimeout(() => controller.abort("probe safety cutoff"), 26000);
      const started = performance.now();
      try {
        const result = await captureBrowserScenario(provider, scenario, { signal: controller.signal });
        const elapsed = Math.round(performance.now() - started);
        expect(result.ok).toBe(false);
        if (result.ok) throw new Error("unexpected success");
        const projected = projectAnalysisError(result.error);
        console.log(JSON.stringify({ selected: timeout ?? "omitted", elapsed_ms: elapsed, never_requests: neverRequests, outcome: projected }));
        expect(projected.code).toBe(timeout === undefined ? "cancelled" : "provider_timeout");
        if (timeout === undefined) expect(elapsed).toBeGreaterThanOrEqual(25000);
        else expect(elapsed).toBeLessThan(10000);
      } finally {
        clearTimeout(abortTimer);
        await provider.close?.();
      }
    }
    expect(neverRequests).toBe(2);
  } finally {
    for (const socket of sockets) socket.destroy();
    await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
  }
}, 45000);

Run the focused source probe with an isolated home and temporary directory:

probe_home=$(mktemp -d)
HOME="$probe_home" TMPDIR="$probe_home" npm run test:local -- src/browser/verify-probe.test.ts
rm src/browser/verify-probe.test.ts
rm -r "$probe_home"

The test asserts the defective baseline behavior so its pass confirms the reproduction. It ran once with each timeout selection. Its safety timer cancels the first capture after 26000 ms, and each run awaits provider cleanup before the fixture closes.

Expected behavior

Expected source: docs/browser-scenario-contract.md:37 says omitting command_timeout_ms retains the 20000 ms default while leaving the field out of Evidence, and explicitly applies that deadline to initial start_url navigation through load. A page whose load never settles should therefore end with a typed timeout after the default navigation deadline and necessary cleanup, without requiring caller cancellation. The preserved omission in Evidence should not disable runtime enforcement.

Impact and scope

P2: an input-dependent liveness failure prevents capture startup from returning its promised timeout. It leaves the caller waiting and produces no scenario steps before cancellation. The explicit-deadline workaround is practical and demonstrated: setting command_timeout_ms to 2000 restored timeout classification for this fixture. Callers can explicitly select their required deadline in scenario JSON, including the documented default. This does not make a page with an unfinished load complete; it restores bounded failure and cleanup.

The independently exercised scope is launch mode with a stalled image response. Source inspection places both CLI and MCP callers on the affected navigation path. Connected-browser mode and MCP client deadline behavior were not runtime-tested. An ordinary streaming fetch does not necessarily delay a page's load event and is not sufficient by itself to trigger this defect. No data loss or security-boundary violation was demonstrated.

REA version

6.8.0; source revision f8f7ac96de.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1 build 26A434; Darwin 27.0.0; arm64. The launched browser reported Chromium 156.0.8078.47.

Target and artifact context

A self-owned loopback HTTP page with an unfinished image response, a provider-owned temporary browser profile, and browser-scenario partial observations before the first step.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

The independent probe printed these selected fields from its results; omitted fields are not shown:

{"selected":"omitted","elapsed_ms":26189,"never_requests":1,"code":"cancelled","cleanup":"complete","duration_ms":26187,"steps":[],"browser_cleanup":"terminated-owned-process"}
{"selected":2000,"elapsed_ms":2512,"never_requests":2,"code":"provider_timeout","timeout_ms":2000,"duration_ms":2512,"steps":[],"browser_cleanup":"terminated-owned-process"}

The focused run exited successfully with one test passed. The browser version and cleanup state came from the returned partial observations. The omitted case was actively cancelled by the probe; it was not observed for an infinite duration.

Failure source: src/browser/PlaywrightScenarioSession.ts:267 starts navigation with waitUntil: "load", disables Playwright's timeout, and passes the raw optional scenario deadline into the execution boundary. Context navigation defaults are also disabled at src/browser/PlaywrightScenarioSession.ts:76. src/browser/PlaywrightExecutionBoundary.ts:28 creates no deadline for an undefined timeout, so its scheduling branch installs no timer. This establishes unbounded navigation waiting from source, beyond the finite runtime observation.

Reachability evidence: src/cliBrowserScenarioCommands.ts:74 invokes the application service after schema validation and normal provider construction. src/cli/commandCancellation.ts:13 supplies signal-driven cancellation without a timer, and src/cli/finishAnalysisCommand.ts:39 awaits the operation. src/server/registerBrowserScenarioTool.ts:52 invokes the same service. src/application/BrowserScenarioCaptureService.ts:40, src/composition/browserScenario.ts:9, and src/browser/PlaywrightBrowserScenarioProvider.ts:263 lead to the production session opener used by the probe.

Analysis or suspected cause

Source-derived cause: navigation receives the optional field without resolving its runtime default. By comparison, browser startup resolves that default at src/browser/PlaywrightScenarioBrowser.ts:857 using src/browser/ScenarioCommandDeadline.ts:26. That earlier phase's timer does not cover the later navigation. Both REA's navigation timer and Playwright's navigation timer are therefore absent for omitted input.

Additional context

The omitted-deadline case was actively cancelled by the probe at 26 seconds; it was not observed for an unbounded duration. Related, distinct: #5364 (closed) concerned cancelling browser scenario startup.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7402 ### 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 A valid browser scenario that omits `command_timeout_ms` keeps waiting for its initial page load beyond the promised 20000 ms default. In an independent real-browser run, a loopback page containing an image whose response never ended remained in startup until the probe cancelled it at 26 seconds. The operation returned `cancelled` after 26189 ms with no captured steps. With the same scenario and `command_timeout_ms: 2000`, it instead returned `provider_timeout` after 2512 ms. I reproduced this through the shared application service and its production Playwright provider with no mocks. The CLI and MCP entrypoints' use of that same path is source-confirmed; I did not independently execute the packaged CLI or an MCP request. ### Reproduction Use a fresh clone at the revision below, Node.js 24.22.1, locked dependencies installed with `npm ci`, and an already installed Chrome-family browser. Save this independent probe as `src/browser/verify-probe.test.ts`. Replace its browser executable with the installed browser's absolute path if needed; the path shown is the one exercised on macOS. The fixture serves only loopback HTTP and intentionally leaves the image response unfinished. ```ts import { createServer } from "node:http"; import type { Socket } from "node:net"; import { expect, it } from "vitest"; import { browserScenarioSchema } from "../domain/browserScenario.js"; import { createBrowserScenarioProvider } from "../composition/browserScenario.js"; import { captureBrowserScenario } from "../application/BrowserScenarioCaptureService.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; it("checks initial navigation with omitted and explicit deadlines against a real local browser", async () => { const sockets = new Set<Socket>(); let neverRequests = 0; const server = createServer((request, response) => { if (request.url === "/never") { neverRequests++; response.writeHead(200, { "content-type": "image/png", "content-length": "100000" }); response.flushHeaders(); } else { response.writeHead(200, { "content-type": "text/html" }); response.end('<!doctype html><img src="/never">'); } }); server.on("connection", socket => { sockets.add(socket); socket.on("close", () => sockets.delete(socket)); }); await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); if (address === null || typeof address === "string") throw new Error("missing listener"); try { for (const timeout of [undefined, 2000]) { const scenario = browserScenarioSchema.parse({ browser: { mode: "launch", executable_path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" }, start_url: { url: `http://127.0.0.1:${address.port}/hang` }, actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }], ...(timeout === undefined ? {} : { command_timeout_ms: timeout }), }); const provider = createBrowserScenarioProvider(process.env); const controller = new AbortController(); const abortTimer = setTimeout(() => controller.abort("probe safety cutoff"), 26000); const started = performance.now(); try { const result = await captureBrowserScenario(provider, scenario, { signal: controller.signal }); const elapsed = Math.round(performance.now() - started); expect(result.ok).toBe(false); if (result.ok) throw new Error("unexpected success"); const projected = projectAnalysisError(result.error); console.log(JSON.stringify({ selected: timeout ?? "omitted", elapsed_ms: elapsed, never_requests: neverRequests, outcome: projected })); expect(projected.code).toBe(timeout === undefined ? "cancelled" : "provider_timeout"); if (timeout === undefined) expect(elapsed).toBeGreaterThanOrEqual(25000); else expect(elapsed).toBeLessThan(10000); } finally { clearTimeout(abortTimer); await provider.close?.(); } } expect(neverRequests).toBe(2); } finally { for (const socket of sockets) socket.destroy(); await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve())); } }, 45000); ``` Run the focused source probe with an isolated home and temporary directory: ```sh probe_home=$(mktemp -d) HOME="$probe_home" TMPDIR="$probe_home" npm run test:local -- src/browser/verify-probe.test.ts rm src/browser/verify-probe.test.ts rm -r "$probe_home" ``` The test asserts the defective baseline behavior so its pass confirms the reproduction. It ran once with each timeout selection. Its safety timer cancels the first capture after 26000 ms, and each run awaits provider cleanup before the fixture closes. ### Expected behavior Expected source: `docs/browser-scenario-contract.md:37` says omitting `command_timeout_ms` retains the 20000 ms default while leaving the field out of Evidence, and explicitly applies that deadline to initial `start_url` navigation through `load`. A page whose load never settles should therefore end with a typed timeout after the default navigation deadline and necessary cleanup, without requiring caller cancellation. The preserved omission in Evidence should not disable runtime enforcement. ### Impact and scope P2: an input-dependent liveness failure prevents capture startup from returning its promised timeout. It leaves the caller waiting and produces no scenario steps before cancellation. The explicit-deadline workaround is practical and demonstrated: setting `command_timeout_ms` to 2000 restored timeout classification for this fixture. Callers can explicitly select their required deadline in scenario JSON, including the documented default. This does not make a page with an unfinished load complete; it restores bounded failure and cleanup. The independently exercised scope is launch mode with a stalled image response. Source inspection places both CLI and MCP callers on the affected navigation path. Connected-browser mode and MCP client deadline behavior were not runtime-tested. An ordinary streaming fetch does not necessarily delay a page's load event and is not sufficient by itself to trigger this defect. No data loss or security-boundary violation was demonstrated. ### REA version 6.8.0; source revision f8f7ac96de8a66a4ab9a23d87e04779bfcd11045. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1 build 26A434; Darwin 27.0.0; arm64. The launched browser reported Chromium 156.0.8078.47. ### Target and artifact context A self-owned loopback HTTP page with an unfinished image response, a provider-owned temporary browser profile, and browser-scenario partial observations before the first step. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence The independent probe printed these selected fields from its results; omitted fields are not shown: ```json {"selected":"omitted","elapsed_ms":26189,"never_requests":1,"code":"cancelled","cleanup":"complete","duration_ms":26187,"steps":[],"browser_cleanup":"terminated-owned-process"} {"selected":2000,"elapsed_ms":2512,"never_requests":2,"code":"provider_timeout","timeout_ms":2000,"duration_ms":2512,"steps":[],"browser_cleanup":"terminated-owned-process"} ``` The focused run exited successfully with one test passed. The browser version and cleanup state came from the returned partial observations. The omitted case was actively cancelled by the probe; it was not observed for an infinite duration. Failure source: `src/browser/PlaywrightScenarioSession.ts:267` starts navigation with `waitUntil: "load"`, disables Playwright's timeout, and passes the raw optional scenario deadline into the execution boundary. Context navigation defaults are also disabled at `src/browser/PlaywrightScenarioSession.ts:76`. `src/browser/PlaywrightExecutionBoundary.ts:28` creates no deadline for an undefined timeout, so its scheduling branch installs no timer. This establishes unbounded navigation waiting from source, beyond the finite runtime observation. Reachability evidence: `src/cliBrowserScenarioCommands.ts:74` invokes the application service after schema validation and normal provider construction. `src/cli/commandCancellation.ts:13` supplies signal-driven cancellation without a timer, and `src/cli/finishAnalysisCommand.ts:39` awaits the operation. `src/server/registerBrowserScenarioTool.ts:52` invokes the same service. `src/application/BrowserScenarioCaptureService.ts:40`, `src/composition/browserScenario.ts:9`, and `src/browser/PlaywrightBrowserScenarioProvider.ts:263` lead to the production session opener used by the probe. ### Analysis or suspected cause Source-derived cause: navigation receives the optional field without resolving its runtime default. By comparison, browser startup resolves that default at `src/browser/PlaywrightScenarioBrowser.ts:857` using `src/browser/ScenarioCommandDeadline.ts:26`. That earlier phase's timer does not cover the later navigation. Both REA's navigation timer and Playwright's navigation timer are therefore absent for omitted input. ### Additional context The omitted-deadline case was actively cancelled by the probe at 26 seconds; it was not observed for an unbounded duration. Related, distinct: #5364 (closed) concerned cancelling browser scenario startup.
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#4409
No description provided.