baa-conductor

git clone 

baa-conductor / bugs / archive
codex@macbookpro  ·  2026-03-27

BUG-012-browser-request-policy-waiter-deadlock.md

  1# BUG-012: browser-request-policy waiter 无超时,slot 泄漏导致目标永久死锁
  2
  3## 状态
  4
  5- 已修复(2026-03-27,代码核对 + 自动化验证)
  6
  7## 当前代码结论
  8
  9- `apps/conductor-daemon/src/browser-request-policy.ts` 已新增 `BROWSER_REQUEST_WAITER_TIMEOUT_MS = 120_000`
 10- target slot 和 platform admission 两类 waiter 都改为经由统一的 `waitForWaiter(...)` 等待,并在超时后从队列里移除自己
 11- 超时会返回 `BrowserRequestPolicyError("waiter_timeout")`,并带 `wait_scope`、`client_id`、`platform`、`timeout_ms`
 12- `apps/conductor-daemon/src/index.test.js` 已补 3 条专项测试:
 13  - target slot waiter timeout
 14  - platform admission waiter timeout
 15  - HTTP `503 browser_risk_limited` 返回路径
 16- 验证结果:
 17  - `pnpm -C /Users/george/code/baa-conductor -F @baa-conductor/conductor-daemon build` 通过
 18  - `node --test /Users/george/code/baa-conductor/apps/conductor-daemon/src/index.test.js` 通过(`31/31`)
 19
 20## 剩余风险
 21
 22- 这次修复解决的是“永久挂起”,不是“自动回收泄漏 slot”
 23- 如果某个 lease 持续泄漏且一直不恢复,同一 `target` 的后续请求会稳定超时失败,而不是自动自愈
 24- 如需继续增强,可后续新增 stale `inFlight` 清扫机制;这不属于本轮修复范围
 25
 26## 修复前现象
 27
 28当某个 browser request 的 policy lease 没有被正常 `complete()` 时(如 Firefox 断连、conductor 内部异常),对同一 `(clientId, platform)` 目标的所有后续请求永久挂起,表现为 `/v1/browser/request``/v1/browser/claude/send` 无响应。
 29
 30- 哪个模块:`apps/conductor-daemon/src/browser-request-policy.ts`
 31- 返回了什么:无返回——请求永久等待
 32- 预期:超时后应该返回错误或自动释放 slot
 33- 是否稳定复现:需要特定异常路径触发,但一旦触发即永久生效
 34
 35## 修复前触发路径
 36
 37```text
 38POST /v1/browser/request (或 /v1/browser/claude/send)
 39-> beginBrowserRequestLease()
 40-> BrowserRequestPolicyController.beginRequest()
 41-> acquireTargetSlot() — 获得 slot,inFlight 变为 1
 42-> admitRequest() — 等待限流/抖动
 43-> 开始执行浏览器代发请求
 44-> Firefox 插件 WS 断连 / conductor 内部 throw 未被 catch
 45-> lease.complete() 从未被调用
 46-> slot 永久被占用(inFlight 始终为 1)
 47-> 后续请求进入 acquireTargetSlot() -> waiters.push(resolve) -> 永不 resolve
 48```
 49
 50## 修复前根因
 51
 52两个独立问题共同导致:
 53
 54### 问题 1:acquireTargetSlot waiter 无超时
 55
 56```typescript
 57private async acquireTargetSlot(state: BrowserRequestTargetState): Promise<void> {
 58  if (state.inFlight < this.config.concurrency.maxInFlightPerClientPlatform
 59      && state.waiters.length === 0) {
 60    state.inFlight += 1;
 61    return;
 62  }
 63  // 这个 Promise 永远没有 reject/timeout 机制
 64  await new Promise<void>((resolve) => {
 65    state.waiters.push(resolve);
 66  });
 67}
 68```
 69
 70### 问题 2:acquirePlatformAdmission 同样无超时
 71
 72```typescript
 73private async acquirePlatformAdmission(state: BrowserRequestPlatformState): Promise<void> {
 74  if (!state.busy) {
 75    state.busy = true;
 76    return;
 77  }
 78  await new Promise<void>((resolve) => {
 79    state.waiters.push(resolve);
 80  });
 81}
 82```
 83
 84### 问题 3:lease 的 complete() 不保证被调用
 85
 86`BrowserRequestPolicyLeaseImpl.complete()` 由调用方手动调用,但如果调用方 throw 了且没有 finally 保护,complete 就不会被调用。
 87
 88## 复现步骤
 89
 90```bash
 91TOKEN="<BAA_SHARED_TOKEN>"
 92
 93# 1. 发起一个 browser request
 94curl -s https://conductor.makefile.so/v1/browser/claude/send \
 95  -X POST -H "Content-Type: application/json" \
 96  -H "Authorization: Bearer $TOKEN" \
 97  -d '{"message":"test"}'
 98
 99# 2. 在请求执行过程中,断开 Firefox 插件的 WS 连接(关闭 Firefox 或重启插件)
100
101# 3. 如果 bridge error 的处理路径没有调 lease.complete(),slot 泄漏
102
103# 4. 后续对 claude 平台的所有 browser request 永久挂起
104curl -s https://conductor.makefile.so/v1/browser/claude/send \
105  -X POST -H "Content-Type: application/json" \
106  -H "Authorization: Bearer $TOKEN" \
107  -d '{"message":"this will hang forever"}'
108```
109
110注意:需要在精确的时间窗口内断开 Firefox 才能触发。但当前 `maxInFlightPerClientPlatform = 1`,意味着只需一次泄漏就会死锁。
111
112## 修复前影响
113
114- 当前是潜在风险,尚未在线上明确观察到
115- `maxInFlightPerClientPlatform = 1` 意味着一次泄漏 = 该 target 永久不可用
116- task scheduler 上线后,并发调用 browser request 的概率增大,触发风险提高
117- 重启 conductor-daemon 可恢复(内存状态清空)
118
119## 修复方案(已落地)
120
121### 方案 A(推荐:多层防御)
122
123同时修三个问题:
124
125**A1. waiter 加超时(120s)**
126
127`acquireTargetSlot``acquirePlatformAdmission` 的 waiter Promise 加超时,超时后从 waiters 数组中移除自己,reject 为 `BrowserRequestPolicyError("slot_wait_timeout")`128
129**A2. lease 加 safety net**
130
131`executeBrowserRequest`(local-api.ts)中确保所有退出路径都调 `lease.complete()`,使用 try/finally 包裹。
132
133**A3. slot 泄漏自愈检测**
134
135定期(例如每 60s)扫描 targets Map,如果某个 target 的 `inFlight > 0` 但最后一次 admit 已超过 5 分钟,强制重置 `inFlight = 0` 并释放一个 waiter。
136
137### 方案 B
138
139只做 A2(保证 finally 调 complete),不改 waiter 超时。更简单但防御层次少。
140
141## 严重程度
142
143Medium
144
145- 当前不常触发,但一旦触发即永久死锁(直到重启)
146- task scheduler 上线后风险升高
147- `maxInFlightPerClientPlatform = 1` 放大了影响——零容错
148
149## 发现时间
150
1512026-03-26 by Claude (code review)
152
153## 备注
154
155- 当前文档保留为问题归档;实际代码已完成 waiter timeout 与清理
156- 本轮没有实现 stale `inFlight` 自动清扫;这是后续增强项,不是未修复状态