12 KiB
Proposal M9: browser / automation 拆出独立 utility process
来源:问题.md M9(行 272-276)、附录 I 第 3 条(行 589-590) 状态:proposed 提出:Roundtable 2026-04-20(Linus + Karpathy + Steve Jobs) Owner: TBD 估计工时:3 周 Blocking dependencies:C2(RuntimeBus 就绪后,跨进程 dispatch 更容易)
问题陈述
src/main/browser/ 和 src/main/automation/ 两个子系统目前运行在主进程:
browser/跑 Electron 的 BrowserWindow / BrowserView,做网页自动化(填表单、点按钮、截图)automation/跑 rule engine,触发 browser actions 或其他 side effects
后果:
- Crash blast radius 大:browser page crash / 内存泄漏直接拖垮 main 进程,整个 desktop app 挂掉
- 事件循环阻塞:自动化脚本跑长时间同步操作(例如等待 CAPTCHA)时,main 进程 UI IPC 都受影响
- Cloudflare / CAPTCHA 等防爬虫对抗:反爬逻辑跑在 main 进程意味着主程序有"机器人"指纹,可能被下游服务整体封禁
- 资源占用不可见:browser 内存和 CPU 混在 main 进程里,不便于监控 / 限额
- Attack surface:自动化脚本触达外部网站,一旦被攻击(XSS / sandbox escape)直接污染 main 进程的数据库连接和文件系统权限
根因分析
Linus 视角:crash blast radius 是进程边界的第一目的。把 browser 跑在 main 进程 = 把不稳定的组件和稳定的组件绑一起了。Electron 提供 utility process 正是为此——crash 就让它 crash,主进程重启它即可。
当前架构把"永远不 crash 的组件(main)"和"经常 crash 的组件(browser automation)"放在同一个进程里——这是 colocation 反模式。
Karpathy 视角:agent 跑 browser automation 是典型的 stochastic task——成功率不是 100%。需要独立的失败域 + 重试机制 + resource cap。跑在 main 进程无法给 browser 设单独的 memory limit / CPU quota。
设计方案
架构
Main process
├── Orchestration / DB / IPC to renderer
│
├──[fork]──> Worker utility process (agent run loop)
│
├──[fork]──> Browser utility process (browser automation)
│ ├── Electron BrowserWindow / Puppeteer
│ ├── 独立内存 / CPU quota
│ └── 通过 bus dispatch "browserAction.dispatch" 消费
│
└──[fork]──> Automation utility process (rule engine)
├── Rule evaluation loop
├── Scheduled triggers
└── 通过 bus dispatch 发送到 browser / agent
对 C2 bus 的依赖
本 proposal 依赖 C2 的 RuntimeBus——拆进程后跨进程 dispatch 最自然的实现是:
- Main 的 bus 是 hub
- utility process 和 main 之间通过
process.parentPort.postMessage传递 command/event - Bus 在 main 侧做 multiplexing:根据 command kind 路由到对应 utility process
这让 C2 的抽象同时覆盖 in-process 和 cross-process 两种 dispatch,代码层面完全一致。
Browser utility process 实现
新建 src/main/browser-process/entry.ts:
import { parentPort } from "node:worker_threads"; // 或 utilityProcess API
const browser = new BrowserAutomationService(...);
parentPort.on("message", async (msg) => {
if (msg.kind === "browserAction.dispatch") {
try {
const result = await browser.execute(msg.payload);
parentPort.postMessage({ kind: "browserAction.result", runId: msg.runId, result });
} catch (e) {
parentPort.postMessage({ kind: "browserAction.error", runId: msg.runId, error: serializeError(e) });
}
}
});
Automation utility process
rule engine 比 browser 轻量,但同样的道理——独立进程 = 独立故障域。新建 src/main/automation-process/entry.ts:
const engine = new AutomationRuleEngine(db);
engine.on("fire", (payload) => {
parentPort.postMessage({ kind: "automation.fired", ...payload });
});
// tick loop
setInterval(() => engine.evaluate(), 1000);
DB 访问
utility process 不直接访问 DB——所有 DB 操作通过 bus 让 main 进程的 writer actor 处理(与 B4 的 single-writer actor 配合)。
这避免 SQLite 跨进程连接的复杂度(SQLite 支持跨进程但要处理 WAL 同步)。
Resource cap
- Electron utility process 可设
memoryLimit(v27+) - 配置
maxOldSpaceSize控制 V8 heap - Main 进程定期 ping utility process,超时自动 kill + restart
Crash recovery
Browser utility process crash 时:
- Main 收到
process.exit事件 - 发"browser 不可用"告警
- 根据 restart policy 重启进程
- 重启后已在运行的 browser action 状态从 DB 恢复
Automation utility process 同理。
Migration 计划
- Phase 0(前置):C2 RuntimeBus 完成
- Phase 1(1 周):设计 cross-process bus 协议(message envelope / error serialization / backpressure)
- Phase 2(1 周):browser 拆进程——browser automation 代码搬到
browser-process/,main 侧改为通过 bus dispatch - Phase 3(3 天):automation 拆进程——同样方式
- Phase 4(3 天):monitoring & restart policy—— main 侧加 utility process health check + telemetry
- Phase 5(1 周观察期):生产压力测试 / crash 注入测试,确认 blast radius 隔离
风险与回滚
| 风险 | 触发条件 | 回滚方法 |
|---|---|---|
| IPC latency 升高 | browserAction 从本地函数调用变成跨进程 | 每次 action 多 1-2ms 可忽略;如果某 action 需要高频通信则重新评估 |
| State 丢失 | utility process crash 时正在执行的 action 状态 | 所有 action 状态进 DB 持久化,crash 后恢复 |
| 开发体验差 | 调试需要 attach 多个进程 | VS Code 多 process debug configuration |
| macOS entitlements | utility process 需要特殊 entitlement 才能跑某些 Electron API | 研究 Apple 文档,提前在 entitlements.plist 加 |
回滚路径:拆进程过程保留旧 in-process 代码路径作为 feature flag(BROWSER_USE_UTILITY_PROCESS=true/false),回滚时切 flag。
验证标准
- Blast radius 验证:故意在 browser utility process 内
process.exit(1),main 进程应不受影响,UI 仍可用 - Memory isolation:browser automation 跑长时间任务,main 进程 memory 稳定不增长
- Restart 自动化:kill -9 browser process,main 检测到后自动重启,丢失 ≤1 个 in-flight action
- Latency baseline:browserAction.dispatch 的 P99 latency 在可接受区间(< 50ms 额外开销)
- Crash rate telemetry:生产观察一周,browser process crash 数据可见且独立于 main 进程崩溃
不在范围
- 不把 worker 也一起重构:worker 已经是 utility process(见 worker-entry.ts),本 proposal 只补 browser / automation
- 不实现跨机器 dispatch:bus 仍是单机进程间,不做分布式
- 不重构 rule engine 的规则 DSL:只是搬进程
- 不做 browser farm:单个 browser utility process 就够,不是并发 N 个实例
- 不改 renderer:renderer 层对 browser / automation 的交互仍走 main → utility process 中转
参考文件清单
src/main/browser/(当前 browser automation 代码)src/main/automation/(当前 automation rule engine 代码)src/main/automation-service.ts(当前 rule engine 入口)src/main/browser/browser-action-service.ts(action 执行)src/main/browser/browser-action-executor.ts(具体 action)src/main/service-graph.ts:110-113(browserActionRef / automationRef 的 LateBinding,C2 会改)- Electron utilityProcess 文档:https://www.electronjs.org/docs/latest/api/utility-process
附:为什么现在修
单个 browser page 的崩溃已经是生产痛点——任何 agent 的网页操作触发 chromium crash 都拖垮用户整个 desktop app。随着 agent 被用于更多 web automation 场景(社交登录、paid subscription 填表、CAPTCHA 对抗),crash 频率只会升。
C2 做完后,跨进程 bus 成本极低——本 proposal 是把 C2 的 bus 抽象延伸到进程边界,很自然。
Jobs 视角:"app 不能崩"是 desktop 软件的基础承诺。当前架构把 desktop 稳定性和 agent browser 稳定性绑定了,这违反了承诺。
附:gating matrix
| Gate | 条件 | 放行判断人 |
|---|---|---|
| G1 | C2 RuntimeBus 完成 + 跨进程 dispatch 协议定稿 | Linus |
| G2 | browser utility process 跑 long-running automation 1 小时无 main 进程回压 | Owner |
| G3 | Kill utility process 后 main 进程自动重启,in-flight action 损失 ≤ 1 | Owner |
| G4 | Browser process 内存隔离验证:独立 heap,不与 main 共享 | Linus |
| G5 | macOS entitlements 配置完整,通过 notarization | Owner |
附:跨进程 message envelope
interface CrossProcessMessage {
id: string; // correlation id
kind: CommandKind | EventKind;
payload: unknown; // 由 kind 决定 shape
source: "main" | "browser-process" | "automation-process" | "worker";
timestamp: number;
// Error case
error?: { name: string; message: string; stack?: string };
}
序列化走 JSON(Electron utilityProcess 默认);大 payload 用 MessagePort + structured clone。
附:跨进程 DB 访问禁令
Utility process 内禁止直接 new DatabaseSync(...) ——所有 DB 操作走 bus dispatch 到 main 的 writer actor(B4)。原因:
- SQLite WAL 跨进程同步复杂
- Main 的 writer actor 保持单 writer 不变式
- Utility process crash 不会导致 DB 文件锁残留
附:反模式对比
Before:
- Main 进程 import browser automation service
- 浏览器 crash → main 进程 crash → desktop app 关闭
- 用户 log off,30+ 秒后重新登录才能恢复
After:
- Main 进程通过 bus dispatch 到 browser utility process
- 浏览器 crash → utility process 独立退出 → main 进程 2 秒内重启它
- 用户看到 "browser automation recovering",UI 其他部分无感
附:为什么不选其他方案
方案 A(当前 proposal):Electron utilityProcess 隔离。
方案 B(完全拆出 desktop app 外,跑独立 daemon):过度工程——用户不希望后台跑多个进程。
方案 C(保留 main 进程但加 try-catch):JS 层的 try-catch 无法 catch native crash(chromium renderer crash)。
方案 A 是 Electron 官方推荐的隔离路径。
附:restart policy 细节
| 情况 | 行为 |
|---|---|
| Browser utility process 正常退出(exit 0) | 不重启(可能是主动 stop) |
| 异常退出(exit ≠ 0) | 立即重启,最多 3 次/分钟 |
| 超过 3 次/分钟 | 进入 backoff(冷却 5 分钟) |
| 冷却期间 browserAction.dispatch | 直接返回 BROWSER_UNAVAILABLE 错误 |
| 冷却结束 | 自动重启;成功则清 backoff 计数 |
| 连续 3 次 backoff 失败 | 告警 + 停止重启,等用户手动恢复 |
附:resource cap 配置
const browserProcess = utilityProcess.fork(entryPath, args, {
serviceName: "agent-company-browser",
stdio: "pipe",
execArgv: ["--max-old-space-size=2048"], // 2GB heap 上限
});
macOS 的 resource monitor 可以看到 agent-company-browser 的 CPU/memory 独立使用,用户也能直接感知 automation 占用。
附:与 C2 的跨进程扩展
C2 的 InProcessRuntimeBus 扩展为支持 cross-process:
class CrossProcessRuntimeBus implements RuntimeBus {
constructor(private child: UtilityProcess, private local: InProcessRuntimeBus) {}
async dispatch(kind, payload) {
if (this.isLocalKind(kind)) {
return this.local.dispatch(kind, payload);
}
return this.sendToChild(kind, payload);
}
}
消费者代码不变——bus 抽象隐藏了进程边界。