fix(daemon): add backpressure control and command serialization to prevent IPC EAGAIN (#529)

- Add AGENT_BROWSER_DEFAULT_TIMEOUT env var to override Playwright's
  default 60s timeout (CDP/recording 10s timeouts unaffected)
- Add backpressure-aware safeWrite() that waits for drain when socket
  buffer is full, preventing data loss under load
- Serialize command execution per socket via queue to prevent concurrent
  writes that cause buffer contention

These daemon-side fixes complement #329 (CLI-side EAGAIN retry) by
addressing the root causes: Playwright operations that outlast the
CLI's IPC timeout, and concurrent socket.write() calls that overflow
the kernel buffer.

Tested with heavy React app (1000+ DOM nodes) — 10 consecutive
snapshot commands complete without os error 35/11.

Refs #322

Co-authored-by: shohu <shohu@users.noreply.github.com>
This commit is contained in:
shohu
2026-02-23 10:06:06 -06:00
committed by GitHub
co-authored by shohu
parent ad6e206a90
commit 16c4ef2da6
2 changed files with 116 additions and 31 deletions
+21 -5
View File
@@ -28,6 +28,22 @@ import {
ENCRYPTION_KEY_ENV,
} from './state-utils.js';
/**
* Returns the default Playwright timeout for standard operations.
* Can be overridden via the AGENT_BROWSER_DEFAULT_TIMEOUT environment variable.
* CDP and recording contexts use a shorter fixed timeout (10s) and are not affected.
*/
function getDefaultTimeout(): number {
const envValue = process.env.AGENT_BROWSER_DEFAULT_TIMEOUT;
if (envValue) {
const parsed = parseInt(envValue, 10);
if (!isNaN(parsed) && parsed > 0) {
return parsed;
}
}
return 60000;
}
// Screencast frame data from CDP
export interface ScreencastFrame {
data: string; // base64 encoded image
@@ -264,7 +280,7 @@ export class BrowserManager {
context = await this.browser.newContext({
...(this.colorScheme && { colorScheme: this.colorScheme }),
});
context.setDefaultTimeout(60000);
context.setDefaultTimeout(getDefaultTimeout());
this.contexts.push(context);
this.setupContextTracking(context);
} else {
@@ -1018,7 +1034,7 @@ export class BrowserManager {
this.kernelSessionId = session.session_id;
this.kernelApiKey = kernelApiKey;
this.browser = browser;
context.setDefaultTimeout(60000);
context.setDefaultTimeout(getDefaultTimeout());
this.contexts.push(context);
this.pages.push(page);
this.activePageIndex = 0;
@@ -1091,7 +1107,7 @@ export class BrowserManager {
this.browserUseSessionId = session.id;
this.browserUseApiKey = browserUseApiKey;
this.browser = browser;
context.setDefaultTimeout(60000);
context.setDefaultTimeout(getDefaultTimeout());
this.contexts.push(context);
this.pages.push(page);
this.activePageIndex = 0;
@@ -1346,7 +1362,7 @@ export class BrowserManager {
});
}
context.setDefaultTimeout(60000);
context.setDefaultTimeout(getDefaultTimeout());
this.contexts.push(context);
this.setupContextTracking(context);
@@ -1678,7 +1694,7 @@ export class BrowserManager {
viewport: viewport === undefined ? { width: 1280, height: 720 } : viewport,
...(this.colorScheme && { colorScheme: this.colorScheme }),
});
context.setDefaultTimeout(60000);
context.setDefaultTimeout(getDefaultTimeout());
this.contexts.push(context);
this.setupContextTracking(context);