Symptom
`js ` and `eval ` (same kernel function under the hood) can hang indefinitely if the user's expression returns a Promise that never resolves — or if the expression triggers a page-level deadlock (broken navigation, beforeunload that waits on something, infinite loop in a handler).
The MCP-side call sees a timeout. The CDP target stays stuck. Worst case the agent gives up and the user sees a stalled tool call.
Surfaced 2026-05-25 by an agent dispatching mouse events on a React-controlled element — the dispatch hung the entire MCP call.
Root cause
`cdp.evaluateJs()` (src/background/cdp_client.ts:509) invokes `Runtime.evaluate` with `awaitPromise: true` and no timeout:
```typescript
await this.send("Runtime.evaluate", {
expression: code,
returnByValue: true,
awaitPromise: true,
});
```
CDP has no built-in cancellation for `Runtime.evaluate`, so a non-resolving Promise parks the whole `await` until the parent client kills the request.
Fix (shipping in 1.3.1)
Add a configurable timeout (default 30s) via `Promise.race`. On timeout, throw a clear error pointing at the cause, and best-effort call `Runtime.terminateExecution` to clean up the stuck evaluator.
Note on naming
`js` and `eval` both route to the same kernel function (src/background/index.ts:1756). The tier difference (`js` is write, `eval` is read) lives at the MCP-server layer (mcp-server/index.ts:58). Both share this hang risk; the fix in `evaluateJs` covers both.
Symptom
`js
` and `eval` (same kernel function under the hood) can hang indefinitely if the user's expression returns a Promise that never resolves — or if the expression triggers a page-level deadlock (broken navigation, beforeunload that waits on something, infinite loop in a handler).The MCP-side call sees a timeout. The CDP target stays stuck. Worst case the agent gives up and the user sees a stalled tool call.
Surfaced 2026-05-25 by an agent dispatching mouse events on a React-controlled element — the dispatch hung the entire MCP call.
Root cause
`cdp.evaluateJs()` (src/background/cdp_client.ts:509) invokes `Runtime.evaluate` with `awaitPromise: true` and no timeout:
```typescript
await this.send("Runtime.evaluate", {
expression: code,
returnByValue: true,
awaitPromise: true,
});
```
CDP has no built-in cancellation for `Runtime.evaluate`, so a non-resolving Promise parks the whole `await` until the parent client kills the request.
Fix (shipping in 1.3.1)
Add a configurable timeout (default 30s) via `Promise.race`. On timeout, throw a clear error pointing at the cause, and best-effort call `Runtime.terminateExecution` to clean up the stuck evaluator.
Note on naming
`js` and `eval` both route to the same kernel function (src/background/index.ts:1756). The tier difference (`js` is write, `eval` is read) lives at the MCP-server layer (mcp-server/index.ts:58). Both share this hang risk; the fix in `evaluateJs` covers both.