[GH-ISSUE #6060] [Bug] observe_web_session waits out its window after fatal CDP protocol failure #3705

Closed
opened 2026-10-31 00:43:13 +00:00 by bsakaguchi · 0 comments
Owner

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

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

An executed observation through CdpBrowserProvider and the repository’s loopback fake CDP endpoint waited approximately its entire 1,000-ms window after the endpoint emitted invalid JSON immediately after replying to Page.setLifecycleEventsEnabled. It then returned BrowserObservationError with reason protocol_error and no successful session. Elapsed time was 1,010.00 ms. The valid-wire control returned window_elapsed after 1,028.54 ms.

The malformed frame arrives after capture is armed; the socket remains open. This is a simulated CDP peer, not an observed incident from Chromium or Electron.

Reproduction

From the source revision below with locked dependencies installed, save this focused test as tests/process-global/webProtocolLifecycle.test.ts and run:

node node_modules/vitest/vitest.mjs run tests/process-global/webProtocolLifecycle.test.ts --maxWorkers=1

The cases below were executed in one six-case file; unrelated cases and imports are removed here. All six characterization cases passed. These two cases also passed when replayed independently from this excerpt. These assertions pin the observed baseline behavior, not a software repair.

import { expect, it } from 'vitest';
import { CdpBrowserProvider } from '../../src/browser/CdpBrowserProvider.js';
import { observeWebSessionInputSchema } from '../../src/domain/browserSession.js';
import { startFakeCdpBrowser } from '../fixtures/fakeCdpBrowser.js';
it.each([false,true])('web observation protocol failure terminates promptly, malformed=%s',async(malformed)=>{
 const browser=await startFakeCdpBrowser(malformed?{malformedEventOnMethod:'Page.setLifecycleEventsEnabled'}:{});
 const provider=new CdpBrowserProvider();const start=performance.now();
 try{
  const result=await provider.observeSession(observeWebSessionInputSchema.parse({cdp_endpoint:browser.endpoint,allowed_origins:[browser.allowedOrigin],target_id:'allowed-page',observation_ms:1000}));
  const elapsed=performance.now()-start;
  console.log(JSON.stringify({area:'web',malformed,elapsed,resultOk:result.ok,...(!result.ok?{tag:result.error._tag,reason:result.error.reason}: {endReason:result.value.window.end_reason})}));
  expect(elapsed).toBeGreaterThanOrEqual(950);
  expect(result.ok).toBe(!malformed);if(!result.ok)expect(result.error).toMatchObject({reason:'protocol_error'});
 }finally{await provider.close();await browser.close();}
});

The test releases its fixtures and any retained resources in finalization.

Expected behavior

A fatal wire failure should end the armed wait promptly and return the existing typed protocol_error. Returning a partial session is not necessary to fix this delay. CdpConnection.onProtocolFailure explicitly notifies fatal parsing failure even with no pending command. The execution collector already consumes that separate failure signal. The browser guide defines an active, cancellable observation window with distinct terminal conditions.

Impact and scope

P2: a known terminal failure leaves the caller waiting the remaining requested duration before the same typed failure can be reported. A one-second delay was reproduced; longer delays follow from the same caller-selected timer, but were not measured here. Caller cancellation is an available escape by source inspection. Natural malformed-wire frequency and real-browser behavior are unknown. No false successful result, target mutation or security-boundary crossing was observed.

REA version

rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e. Fetched origin/main at 1f92c773989585a7c0f3255b7a9264789394908c; the producing code remains unchanged. That newer revision was source-compared, not executed.

Runtime and platform

macOS 27.0.1 arm64; Node.js v26.12.0; locked Vitest 5.0.3. The repository-pinned Node.js 24.18.0 was not exercised.

Target and artifact context

An allowed synthetic page on the repository’s HTTP/WebSocket CDP fixture, with observation_ms=1000. The failure hook emits invalid JSON after Page.setLifecycleEventsEnabled replies and leaves the peer socket open.

Interface

Other

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

Observed characterization results:

valid-wire control: elapsed=1028.54 ms, result.ok=true, end_reason=window_elapsed
invalid JSON after arm reply: elapsed=1010.00 ms, result.ok=false
error._tag=BrowserObservationError, error.reason=protocol_error

Production-provider behavior was executed. CLI/MCP reachability is source-derived; their transports were not separately invoked.

Typed outcome or recovery expected

Return BrowserObservationError(reason=protocol_error) promptly when the armed connection becomes protocol-failed, releasing the wait timer and listeners. Whether to expose partial timeline data is a separate product choice; this issue does not require a new public end-reason value.

Analysis or suspected cause

Source-derived: observeCdpSession subscribes only to onDisconnect. The wait ends on capture termination, cancellation or elapsed time. Protocol failure is a separate terminal state that does not close the socket or notify disconnect listeners. Only the later final URL command observes that state. The fake endpoint’s failure hook sends the invalid frame after the arm command reply.

Additional context

Compared #5946, #5947, #5954, #5989, #5993 and #5994: these concern missing secrets, target-selection recovery, retention-budget classification, setup cleanup precedence, Electron prefix diagnostics and screenshot scope recovery. Closed #2538 concerns the separate Inspector collector; #3553/PR #3555 concern pending handshake/RPC deadlines; #2555 concerns reply identity validation. #3929/#4392 and PR #5605 concern timer overflow. PR #4040’s current observation patch changes URL identity/evidence handling and timer regressions, not onProtocolFailure subscription. The other passive capture/discovery windows were not executed and are outside this report.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6060 ### 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 An executed observation through CdpBrowserProvider and the repository’s loopback fake CDP endpoint waited approximately its entire 1,000-ms window after the endpoint emitted invalid JSON immediately after replying to Page.setLifecycleEventsEnabled. It then returned BrowserObservationError with reason protocol_error and no successful session. Elapsed time was 1,010.00 ms. The valid-wire control returned window_elapsed after 1,028.54 ms. The malformed frame arrives after capture is armed; the socket remains open. This is a simulated CDP peer, not an observed incident from Chromium or Electron. ### Reproduction From the source revision below with locked dependencies installed, save this focused test as `tests/process-global/webProtocolLifecycle.test.ts` and run: ```sh node node_modules/vitest/vitest.mjs run tests/process-global/webProtocolLifecycle.test.ts --maxWorkers=1 ``` The cases below were executed in one six-case file; unrelated cases and imports are removed here. All six characterization cases passed. These two cases also passed when replayed independently from this excerpt. These assertions pin the observed baseline behavior, not a software repair. ```ts import { expect, it } from 'vitest'; import { CdpBrowserProvider } from '../../src/browser/CdpBrowserProvider.js'; import { observeWebSessionInputSchema } from '../../src/domain/browserSession.js'; import { startFakeCdpBrowser } from '../fixtures/fakeCdpBrowser.js'; it.each([false,true])('web observation protocol failure terminates promptly, malformed=%s',async(malformed)=>{ const browser=await startFakeCdpBrowser(malformed?{malformedEventOnMethod:'Page.setLifecycleEventsEnabled'}:{}); const provider=new CdpBrowserProvider();const start=performance.now(); try{ const result=await provider.observeSession(observeWebSessionInputSchema.parse({cdp_endpoint:browser.endpoint,allowed_origins:[browser.allowedOrigin],target_id:'allowed-page',observation_ms:1000})); const elapsed=performance.now()-start; console.log(JSON.stringify({area:'web',malformed,elapsed,resultOk:result.ok,...(!result.ok?{tag:result.error._tag,reason:result.error.reason}: {endReason:result.value.window.end_reason})})); expect(elapsed).toBeGreaterThanOrEqual(950); expect(result.ok).toBe(!malformed);if(!result.ok)expect(result.error).toMatchObject({reason:'protocol_error'}); }finally{await provider.close();await browser.close();} }); ``` The test releases its fixtures and any retained resources in finalization. ### Expected behavior A fatal wire failure should end the armed wait promptly and return the existing typed `protocol_error`. Returning a partial session is not necessary to fix this delay. [CdpConnection.onProtocolFailure](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/browser/CdpConnection.ts#L137-L147) explicitly notifies fatal parsing failure even with no pending command. [The execution collector](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/browser/execution/CdpExecutionCollection.ts#L43-L48) already consumes that separate failure signal. [The browser guide](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/browser-observation.md#L220-L223) defines an active, cancellable observation window with distinct terminal conditions. ### Impact and scope P2: a known terminal failure leaves the caller waiting the remaining requested duration before the same typed failure can be reported. A one-second delay was reproduced; longer delays follow from the same caller-selected timer, but were not measured here. Caller cancellation is an available escape by source inspection. Natural malformed-wire frequency and real-browser behavior are unknown. No false successful result, target mutation or security-boundary crossing was observed. ### REA version `rea-agents` 6.7.0; source revision `52415975942a4b9c53a817915b313ccc404f280e`. Fetched `origin/main` at `1f92c773989585a7c0f3255b7a9264789394908c`; the producing code remains unchanged. That newer revision was source-compared, not executed. ### Runtime and platform macOS 27.0.1 arm64; Node.js v26.12.0; locked Vitest 5.0.3. The repository-pinned Node.js 24.18.0 was not exercised. ### Target and artifact context An allowed synthetic page on the repository’s HTTP/WebSocket CDP fixture, with observation_ms=1000. The failure hook emits invalid JSON after Page.setLifecycleEventsEnabled replies and leaves the peer socket open. ### Interface Other ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence Observed characterization results: ```text valid-wire control: elapsed=1028.54 ms, result.ok=true, end_reason=window_elapsed invalid JSON after arm reply: elapsed=1010.00 ms, result.ok=false error._tag=BrowserObservationError, error.reason=protocol_error ``` Production-provider behavior was executed. CLI/MCP reachability is source-derived; their transports were not separately invoked. ### Typed outcome or recovery expected Return BrowserObservationError(reason=protocol_error) promptly when the armed connection becomes protocol-failed, releasing the wait timer and listeners. Whether to expose partial timeline data is a separate product choice; this issue does not require a new public end-reason value. ### Analysis or suspected cause Source-derived: [observeCdpSession](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/browser/CdpObservationSession.ts#L71-L73) subscribes only to onDisconnect. [The wait](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/browser/CdpObservationSession.ts#L593-L631) ends on capture termination, cancellation or elapsed time. [Protocol failure](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/browser/CdpConnection.ts#L395-L409) is a separate terminal state that does not close the socket or notify disconnect listeners. Only the later [final URL command](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/browser/CdpObservationSession.ts#L633-L644) observes that state. The fake endpoint’s [failure hook](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/tests/fixtures/fakeCdpBrowser.ts#L229-L235) sends the invalid frame after the arm command reply. ### Additional context Compared #5946, #5947, #5954, #5989, #5993 and #5994: these concern missing secrets, target-selection recovery, retention-budget classification, setup cleanup precedence, Electron prefix diagnostics and screenshot scope recovery. Closed #2538 concerns the separate Inspector collector; #3553/PR #3555 concern pending handshake/RPC deadlines; #2555 concerns reply identity validation. #3929/#4392 and PR #5605 concern timer overflow. PR #4040’s current observation patch changes URL identity/evidence handling and timer regressions, not onProtocolFailure subscription. The other passive capture/discovery windows were not executed and are outside this report.
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#3705
No description provided.