mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 17:28:08 +00:00
* feat(browser): reuse admitted browser-socket transport and allow discovery_timeout_ms * fix(browser): close unhealthy shared sockets and clamp max discovery timer * fix(browser): retry failed shared-socket cleanup while peer leases remain active * test(browser): race observation readiness with awaitAcknowledgement and join before mock restore * fix(browser): isolate shared-socket discovery aborts and coalesce cold connects * fix(browser): observe pending cold-connect cleanup failures and isolate fake CDP sessions * refactor(browser): carry typed connect cleanup owners and gate concurrent session test * test(browser): cover loopback socket reuse-key normalization * test(browser): verify distinct browser socket path IDs stay separate
202 lines
9.3 KiB
TypeScript
202 lines
9.3 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import type { BrowserObservationOperation } from "./browserObservationErrors.js";
|
|
|
|
/** Largest delay supported without overflowing Node timers. */
|
|
export const MAX_BROWSER_DEADLINE_MS = 2_147_483_647;
|
|
|
|
/**
|
|
* CDP command and WebSocket handshake deadline. Discovery and cleanup grace
|
|
* are separate phases and stay fixed.
|
|
*/
|
|
export const DEFAULT_CDP_COMMAND_TIMEOUT_MS = 20_000;
|
|
|
|
/** Per-response deadline for loopback CDP discovery documents. */
|
|
export const CDP_DISCOVERY_RESPONSE_TIMEOUT_MS = 10_000;
|
|
|
|
/** Cleanup grace after a CDP runtime command. It does not extend the command deadline. */
|
|
export const CDP_CLEANUP_GRACE_MS = 5_000;
|
|
|
|
/** Optional caller deadline. An omitted field stays absent in Evidence. */
|
|
export const optionalBrowserDeadlineMs = (description: string) =>
|
|
z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(MAX_BROWSER_DEADLINE_MS)
|
|
.optional()
|
|
.describe(description);
|
|
|
|
export const selectedBrowserDeadlineMs = (
|
|
selected: number | undefined,
|
|
defaultMs: number,
|
|
): number => selected ?? defaultMs;
|
|
|
|
export const browserDeadlineIncrease = (
|
|
key: string,
|
|
timeoutMs: number,
|
|
defaultMs: number,
|
|
cli: string,
|
|
): string =>
|
|
timeoutMs < MAX_BROWSER_DEADLINE_MS
|
|
? `Increase ${key} above ${String(timeoutMs)} (integer milliseconds from 1 to ${String(MAX_BROWSER_DEADLINE_MS)}; omitted means ${String(defaultMs)}), or use ${cli} on the CLI.`
|
|
: `${key} is already at the maximum ${String(MAX_BROWSER_DEADLINE_MS)} ms; do not increase it further.`;
|
|
|
|
export const browserDeadlineRetry =
|
|
"Applies on the next call. Allow execution, cleanup and delivery headroom in the outer MCP client deadline. Confirm outstanding work and cleanup before retrying.";
|
|
|
|
/** Fixed allowance the caller cannot raise through a sibling deadline. */
|
|
export const noRuntimeOverrideSentence = (
|
|
allowance: string,
|
|
unrelatedControl?: string,
|
|
): string =>
|
|
`${allowance} This release has no runtime override.${
|
|
unrelatedControl === undefined
|
|
? ""
|
|
: ` ${unrelatedControl} does not raise it.`
|
|
}`;
|
|
|
|
/** Handshake and per-command deadline shared by CDP browser tools. */
|
|
export const webRuntimeCommandTimeoutSchema = optionalBrowserDeadlineMs(
|
|
`CDP WebSocket handshake and per-command response deadline in milliseconds (default ${String(DEFAULT_CDP_COMMAND_TIMEOUT_MS)}). Omitted means ${String(DEFAULT_CDP_COMMAND_TIMEOUT_MS)} and applies on the next call. deadlineExpired for a handshake or command is not the caller observation window, not the CDP discovery deadline (default ${String(CDP_DISCOVERY_RESPONSE_TIMEOUT_MS)} ms, or discovery_timeout_ms where supported), and not the ${String(CDP_CLEANUP_GRACE_MS)} ms cleanup grace. It also bounds passive script-source collection and the inspection/source-join phase that share this deadline. Does not raise the fixed protocol, retained-event, or source byte budgets, or the cleanup grace, which have no runtime override.`,
|
|
);
|
|
|
|
/** Per-response HTTP discovery and browser-socket discovery deadline. */
|
|
export const browserDiscoveryTimeoutSchema = optionalBrowserDeadlineMs(
|
|
`Loopback CDP HTTP discovery per-response deadline and browser-socket handshake plus discovery (Browser.getVersion, Target.getTargets) deadline in milliseconds (default ${String(CDP_DISCOVERY_RESPONSE_TIMEOUT_MS)}). Omitted means ${String(CDP_DISCOVERY_RESPONSE_TIMEOUT_MS)} and applies on the next call. Separate from command_timeout_ms, the caller observation window, and the ${String(CDP_CLEANUP_GRACE_MS)} ms cleanup grace. Does not raise the fixed HTTP discovery document or WebSocket protocol byte budgets, which have no runtime override.`,
|
|
);
|
|
|
|
export type CdpCommandDeadlinePhase = "handshake" | "command" | "source-join";
|
|
|
|
/**
|
|
* Operations whose public input accepts discovery_timeout_ms. Other CDP callers
|
|
* keep the fixed numeric discovery deadline and must not advertise a setting they lack.
|
|
*/
|
|
const DISCOVERY_TIMEOUT_OPERATIONS = new Set<BrowserObservationOperation>([
|
|
"list_browser_targets",
|
|
"inspect_web_page",
|
|
"analyze_web_bundle",
|
|
"observe_web_session",
|
|
"discover_webmcp_tools",
|
|
"capture_web_screenshot",
|
|
]);
|
|
|
|
export const cdpDiscoveryTimeoutIsSelectable = (
|
|
operation: BrowserObservationOperation,
|
|
): boolean => DISCOVERY_TIMEOUT_OPERATIONS.has(operation);
|
|
|
|
/**
|
|
* Operations whose public input accepts command_timeout_ms. Other CDP callers
|
|
* keep the same numeric deadline and must not advertise a setting they lack.
|
|
*/
|
|
const COMMAND_TIMEOUT_OPERATIONS = new Set<BrowserObservationOperation>([
|
|
"inspect_web_page",
|
|
"analyze_web_bundle",
|
|
"observe_web_session",
|
|
"observe_web_execution",
|
|
"inspect_web_event_listeners",
|
|
"discover_webmcp_tools",
|
|
"capture_web_screenshot",
|
|
"inspect_electron_page",
|
|
"capture_browser_scenario",
|
|
]);
|
|
|
|
export const cdpCommandTimeoutIsSelectable = (
|
|
operation: BrowserObservationOperation,
|
|
): boolean => COMMAND_TIMEOUT_OPERATIONS.has(operation);
|
|
|
|
/** Name command_timeout_ms as an unrelated control only when that operation accepts it. */
|
|
export const cdpCommandTimeoutUnrelatedControl = (
|
|
operation: BrowserObservationOperation,
|
|
): string | undefined =>
|
|
cdpCommandTimeoutIsSelectable(operation) ? "command_timeout_ms" : undefined;
|
|
|
|
const cdpDeadlineSubject = (
|
|
timeoutMs: number,
|
|
phase: CdpCommandDeadlinePhase,
|
|
method: string | undefined,
|
|
selectable: boolean,
|
|
): string => {
|
|
const budget = selectable
|
|
? `${String(timeoutMs)} ms command_timeout_ms`
|
|
: `fixed ${String(timeoutMs)} ms`;
|
|
if (phase === "handshake")
|
|
return `CDP WebSocket handshake exceeded its ${budget} deadline (deadlineExpired).`;
|
|
if (phase === "source-join")
|
|
return `The inspection/source-join phase exceeded its ${budget} deadline (deadlineExpired).`;
|
|
return `CDP command ${method ?? "request"} exceeded its ${budget} response deadline (deadlineExpired).`;
|
|
};
|
|
|
|
/** Name the CDP deadline, and name command_timeout_ms only when the operation accepts it. */
|
|
export const cdpCommandDeadlineRecovery = (
|
|
timeoutMs: number,
|
|
phase: CdpCommandDeadlinePhase,
|
|
method?: string,
|
|
selectable = true,
|
|
): { readonly userMessage: string; readonly remediationAction: string } => {
|
|
const subject = cdpDeadlineSubject(timeoutMs, phase, method, selectable);
|
|
if (!selectable) {
|
|
const fixed = noRuntimeOverrideSentence(
|
|
`${subject} This is not the caller observation window and not the ${String(CDP_DISCOVERY_RESPONSE_TIMEOUT_MS)} ms CDP discovery deadline.`,
|
|
);
|
|
return { userMessage: fixed, remediationAction: fixed };
|
|
}
|
|
return {
|
|
userMessage: `${subject} This is not the caller observation window, not the ${String(CDP_DISCOVERY_RESPONSE_TIMEOUT_MS)} ms CDP discovery deadline, and not the ${String(CDP_CLEANUP_GRACE_MS)} ms cleanup grace.`,
|
|
remediationAction: `${browserDeadlineIncrease("command_timeout_ms", timeoutMs, DEFAULT_CDP_COMMAND_TIMEOUT_MS, "--command-timeout-ms")} ${browserDeadlineRetry} command_timeout_ms does not raise the CDP discovery deadline (default ${String(CDP_DISCOVERY_RESPONSE_TIMEOUT_MS)} ms, or discovery_timeout_ms where supported), nor the fixed protocol, retained-event, or source byte budgets or the ${String(CDP_CLEANUP_GRACE_MS)} ms cleanup grace, which have no runtime override.`,
|
|
};
|
|
};
|
|
|
|
/** Name the CDP discovery deadline, and name discovery_timeout_ms only when the operation accepts it. */
|
|
export const cdpDiscoveryTimeoutRecovery = (
|
|
operation: BrowserObservationOperation,
|
|
timeoutMs: number,
|
|
phase: "response" | "browser-socket",
|
|
budgetDescription: string,
|
|
): { readonly userMessage: string; readonly remediationAction: string } => {
|
|
const unrelatedCommand = cdpCommandTimeoutUnrelatedControl(operation);
|
|
if (!cdpDiscoveryTimeoutIsSelectable(operation)) {
|
|
const subject =
|
|
phase === "response"
|
|
? `CDP discovery response exceeded its ${String(timeoutMs)} ms deadline.`
|
|
: `CDP browser-socket discovery exceeded its ${String(timeoutMs)} ms deadline.`;
|
|
const fixed = noRuntimeOverrideSentence(
|
|
`${budgetDescription} and the ${String(timeoutMs)} ms discovery deadline stay fixed.`,
|
|
unrelatedCommand,
|
|
);
|
|
return {
|
|
userMessage: `${subject} ${fixed}`,
|
|
remediationAction: fixed,
|
|
};
|
|
}
|
|
const subject =
|
|
phase === "response"
|
|
? `CDP discovery response exceeded its ${String(timeoutMs)} ms discovery_timeout_ms deadline (deadlineExpired).`
|
|
: `CDP browser-socket discovery exceeded its ${String(timeoutMs)} ms discovery_timeout_ms deadline (deadlineExpired).`;
|
|
const fixedBudget = noRuntimeOverrideSentence(
|
|
`${budgetDescription} stays fixed.`,
|
|
unrelatedCommand,
|
|
);
|
|
return {
|
|
userMessage: `${subject} This is not the caller observation window and not the ${String(CDP_CLEANUP_GRACE_MS)} ms cleanup grace. ${fixedBudget}`,
|
|
remediationAction: `${browserDeadlineIncrease("discovery_timeout_ms", timeoutMs, CDP_DISCOVERY_RESPONSE_TIMEOUT_MS, "--discovery-timeout-ms")} ${browserDeadlineRetry} ${fixedBudget}`,
|
|
};
|
|
};
|
|
|
|
/** Cleanup grace is a fixed phase after the command deadline. */
|
|
export const cdpCleanupGraceExpired = (): string =>
|
|
noRuntimeOverrideSentence(
|
|
`CDP runtime cleanup exceeded its ${String(CDP_CLEANUP_GRACE_MS)} ms cleanup grace.`,
|
|
"command_timeout_ms",
|
|
);
|
|
|
|
export const selectedCdpCommandTimeoutMs = (
|
|
selected: number | undefined,
|
|
): number =>
|
|
selectedBrowserDeadlineMs(selected, DEFAULT_CDP_COMMAND_TIMEOUT_MS);
|
|
|
|
export const selectedCdpDiscoveryTimeoutMs = (
|
|
selected: number | undefined,
|
|
): number =>
|
|
selectedBrowserDeadlineMs(selected, CDP_DISCOVERY_RESPONSE_TIMEOUT_MS);
|