baa-conductor

git clone 

commit
c87af13
parent
7ba57c9
author
jiaozhiwang
date
2026-03-26 20:22:30 +0800 CST
docs: add BUG-011/012/013 and code review discuss
4 files changed,  +631, -0
A bugs/BUG-011-writeHttpResponse-drain-handler-hangs.md
+112, -0
  1@@ -0,0 +1,112 @@
  2+# BUG-011: writeHttpResponse drain handler 可永久挂起
  3+
  4+## 现象
  5+
  6+当慢客户端或代理中间人在 HTTP 响应写入过程中断连时,conductor-daemon 的请求处理协程永远不返回,表现为请求计数只增不减、内存缓慢增长。
  7+
  8+- 哪个模块:`apps/conductor-daemon/src/index.ts`,`writeHttpResponse()` 函数
  9+- 返回了什么:无返回——协程挂死
 10+- 预期:连接断开后协程应正常退出
 11+- 复现条件:需要客户端在 write 返回 false(背压)后、drain 事件触发前断开连接
 12+
 13+## 触发路径
 14+
 15+```text
 16+HTTP client -> conductor local API (any endpoint with large body or SSE stream)
 17+-> writeHttpResponse()
 18+-> response.write(chunk) returns false (backpressure)
 19+-> await new Promise(resolve => response.on("drain", resolve))
 20+-> client disconnects before drain fires
 21+-> Promise never resolves
 22+-> coroutine hangs forever
 23+```
 24+
 25+## 根因
 26+
 27+`writeHttpResponse()` 中有两处 drain 等待逻辑:
 28+
 29+1. 第 487 行附近:写 `payload.body` 时
 30+2. 第 504 行附近:streamBody 循环中写每个 chunk 时
 31+
 32+两处都只监听了 `drain` 事件,没有监听 `close` / `error` 事件。Node.js HTTP response 在客户端断连后不会再触发 drain,只会触发 close。
 33+
 34+```typescript
 35+// 当前代码
 36+if (!writableResponse.write(payload.body)) {
 37+  await new Promise<void>((resolve) => {
 38+    writableResponse.on?.("drain", resolve);
 39+  });
 40+}
 41+```
 42+
 43+## 复现步骤
 44+
 45+```bash
 46+# 1. 启动一个返回大 body 的请求(如 /v1/status/ui 的 HTML 页面)
 47+# 2. 用 nc 模拟慢客户端,收到 headers 后立即断开
 48+{
 49+  echo -e "GET /v1/status/ui HTTP/1.1\r\nHost: localhost\r\n\r\n";
 50+  sleep 0.1;
 51+} | nc 100.71.210.78 4317 | head -c 100
 52+# 3. 如果 response body > socket buffer,write 会返回 false
 53+# 4. nc 已断开,drain 永远不触发
 54+# 5. 对应的 handler 协程永远不退出
 55+```
 56+
 57+注意:在正常网络条件下不容易触发,需要 body 大到触发背压。SSE stream 路径更容易触发。
 58+
 59+## 当前影响
 60+
 61+- 主流程不受影响(需要特定条件触发)
 62+- SSE stream(browser proxy 返回 claude.ai 的 SSE 流)是最可能的触发场景
 63+- 在 nginx 反代后面运行时,nginx 主动关闭超时连接可触发
 64+- 长期运行会导致协程泄漏和内存缓慢增长
 65+
 66+## 修复建议
 67+
 68+### 方案 A(推荐)
 69+
 70+同时监听 drain 和 close/error,任一触发即 resolve:
 71+
 72+```typescript
 73+async function awaitDrain(response: ServerResponse): Promise<void> {
 74+  if (response.destroyed) return;
 75+  await new Promise<void>((resolve) => {
 76+    const onDrain = () => { cleanup(); resolve(); };
 77+    const onClose = () => { cleanup(); resolve(); };
 78+    const onError = () => { cleanup(); resolve(); };
 79+    const cleanup = () => {
 80+      response.off("drain", onDrain);
 81+      response.off("close", onClose);
 82+      response.off("error", onError);
 83+    };
 84+    response.on("drain", onDrain);
 85+    response.on("close", onClose);
 86+    response.on("error", onError);
 87+  });
 88+}
 89+```
 90+
 91+将 `writeHttpResponse()` 中两处 drain 等待替换为调用此函数。
 92+
 93+### 方案 B
 94+
 95+给 drain 等待加超时(例如 30s),超时后直接 resolve 并终止写入。简单但不够精确。
 96+
 97+## 严重程度
 98+
 99+Medium-High
100+
101+- 生产环境中 nginx 超时断连、客户端网络不稳定时会触发
102+- SSE stream 路径最容易触发
103+- 泄漏的协程和连接对象不会被 GC,长期运行有 OOM 风险
104+
105+## 发现时间
106+
107+2026-03-26 by Claude (code review)
108+
109+## 备注
110+
111+- 当前未在线上明确观察到,但 nginx 502 超时场景下可能已经触发过
112+- streamBody 循环中的同一模式也需要同步修复
113+- 修复后建议加单元测试:mock 一个 write 返回 false 后立即 destroy 的 response 对象
A bugs/BUG-012-browser-request-policy-waiter-deadlock.md
+134, -0
  1@@ -0,0 +1,134 @@
  2+# BUG-012: browser-request-policy waiter 无超时,slot 泄漏导致目标永久死锁
  3+
  4+## 现象
  5+
  6+当某个 browser request 的 policy lease 没有被正常 `complete()` 时(如 Firefox 断连、conductor 内部异常),对同一 `(clientId, platform)` 目标的所有后续请求永久挂起,表现为 `/v1/browser/request` 或 `/v1/browser/claude/send` 无响应。
  7+
  8+- 哪个模块:`apps/conductor-daemon/src/browser-request-policy.ts`
  9+- 返回了什么:无返回——请求永久等待
 10+- 预期:超时后应该返回错误或自动释放 slot
 11+- 是否稳定复现:需要特定异常路径触发,但一旦触发即永久生效
 12+
 13+## 触发路径
 14+
 15+```text
 16+POST /v1/browser/request (或 /v1/browser/claude/send)
 17+-> beginBrowserRequestLease()
 18+-> BrowserRequestPolicyController.beginRequest()
 19+-> acquireTargetSlot() — 获得 slot,inFlight 变为 1
 20+-> admitRequest() — 等待限流/抖动
 21+-> 开始执行浏览器代发请求
 22+-> Firefox 插件 WS 断连 / conductor 内部 throw 未被 catch
 23+-> lease.complete() 从未被调用
 24+-> slot 永久被占用(inFlight 始终为 1)
 25+-> 后续请求进入 acquireTargetSlot() -> waiters.push(resolve) -> 永不 resolve
 26+```
 27+
 28+## 根因
 29+
 30+两个独立问题共同导致:
 31+
 32+### 问题 1:acquireTargetSlot waiter 无超时
 33+
 34+```typescript
 35+private async acquireTargetSlot(state: BrowserRequestTargetState): Promise<void> {
 36+  if (state.inFlight < this.config.concurrency.maxInFlightPerClientPlatform
 37+      && state.waiters.length === 0) {
 38+    state.inFlight += 1;
 39+    return;
 40+  }
 41+  // 这个 Promise 永远没有 reject/timeout 机制
 42+  await new Promise<void>((resolve) => {
 43+    state.waiters.push(resolve);
 44+  });
 45+}
 46+```
 47+
 48+### 问题 2:acquirePlatformAdmission 同样无超时
 49+
 50+```typescript
 51+private async acquirePlatformAdmission(state: BrowserRequestPlatformState): Promise<void> {
 52+  if (!state.busy) {
 53+    state.busy = true;
 54+    return;
 55+  }
 56+  await new Promise<void>((resolve) => {
 57+    state.waiters.push(resolve);
 58+  });
 59+}
 60+```
 61+
 62+### 问题 3:lease 的 complete() 不保证被调用
 63+
 64+`BrowserRequestPolicyLeaseImpl.complete()` 由调用方手动调用,但如果调用方 throw 了且没有 finally 保护,complete 就不会被调用。
 65+
 66+## 复现步骤
 67+
 68+```bash
 69+TOKEN="<BAA_SHARED_TOKEN>"
 70+
 71+# 1. 发起一个 browser request
 72+curl -s https://conductor.makefile.so/v1/browser/claude/send \
 73+  -X POST -H "Content-Type: application/json" \
 74+  -H "Authorization: Bearer $TOKEN" \
 75+  -d '{"message":"test"}'
 76+
 77+# 2. 在请求执行过程中,断开 Firefox 插件的 WS 连接(关闭 Firefox 或重启插件)
 78+
 79+# 3. 如果 bridge error 的处理路径没有调 lease.complete(),slot 泄漏
 80+
 81+# 4. 后续对 claude 平台的所有 browser request 永久挂起
 82+curl -s https://conductor.makefile.so/v1/browser/claude/send \
 83+  -X POST -H "Content-Type: application/json" \
 84+  -H "Authorization: Bearer $TOKEN" \
 85+  -d '{"message":"this will hang forever"}'
 86+```
 87+
 88+注意:需要在精确的时间窗口内断开 Firefox 才能触发。但当前 `maxInFlightPerClientPlatform = 1`,意味着只需一次泄漏就会死锁。
 89+
 90+## 当前影响
 91+
 92+- 当前是潜在风险,尚未在线上明确观察到
 93+- `maxInFlightPerClientPlatform = 1` 意味着一次泄漏 = 该 target 永久不可用
 94+- task scheduler 上线后,并发调用 browser request 的概率增大,触发风险提高
 95+- 重启 conductor-daemon 可恢复(内存状态清空)
 96+
 97+## 修复建议
 98+
 99+### 方案 A(推荐:多层防御)
100+
101+同时修三个问题:
102+
103+**A1. waiter 加超时(120s)**
104+
105+给 `acquireTargetSlot` 和 `acquirePlatformAdmission` 的 waiter Promise 加超时,超时后从 waiters 数组中移除自己,reject 为 `BrowserRequestPolicyError("slot_wait_timeout")`。
106+
107+**A2. lease 加 safety net**
108+
109+在 `executeBrowserRequest`(local-api.ts)中确保所有退出路径都调 `lease.complete()`,使用 try/finally 包裹。
110+
111+**A3. slot 泄漏自愈检测**
112+
113+定期(例如每 60s)扫描 targets Map,如果某个 target 的 `inFlight > 0` 但最后一次 admit 已超过 5 分钟,强制重置 `inFlight = 0` 并释放一个 waiter。
114+
115+### 方案 B
116+
117+只做 A2(保证 finally 调 complete),不改 waiter 超时。更简单但防御层次少。
118+
119+## 严重程度
120+
121+Medium
122+
123+- 当前不常触发,但一旦触发即永久死锁(直到重启)
124+- task scheduler 上线后风险升高
125+- `maxInFlightPerClientPlatform = 1` 放大了影响——零容错
126+
127+## 发现时间
128+
129+2026-03-26 by Claude (code review)
130+
131+## 备注
132+
133+- 可以通过 `/v1/browser/status` 返回的 policy snapshot 中的 `targets[].inFlight` 和 `targets[].waiting` 观测是否已经发生泄漏
134+- 临时绕过方式:重启 conductor-daemon
135+- 修复后建议补测试:mock 一个 beginRequest 后不调 complete 的场景,验证超时后 waiter 被正确释放
A bugs/BUG-013-stream-session-timer-not-cleared.md
+112, -0
  1@@ -0,0 +1,112 @@
  2+# BUG-013: firefox-bridge stream session 关闭时未清除 openTimer/idleTimer
  3+
  4+## 现象
  5+
  6+stream session 关闭后,先前设置的 `openTimer` 和 `idleTimer` 仍在运行。timer 触发时由于 `closed` flag 保护不会产生功能异常,但 session 对象在 timer 到期前无法被 GC。
  7+
  8+- 哪个模块:`apps/conductor-daemon/src/firefox-bridge.ts`,`FirefoxBridgeApiStreamSession` 类
  9+- 返回了什么:功能正常,但存在不必要的内存驻留
 10+- 预期:session 关闭时应立即清除所有 timer
 11+- 是否稳定复现:每次 stream session 正常结束或异常关闭都会触发
 12+
 13+## 触发路径
 14+
 15+```text
 16+POST /v1/browser/request (responseMode: "sse")
 17+-> 创建 FirefoxBridgeApiStreamSession
 18+-> resetOpenTimer() — 设置 openTimeoutMs timer
 19+-> 收到 stream_open → resetIdleTimer() — 设置 idleTimeoutMs timer
 20+-> 收到 stream_end → finishWithEvent()
 21+-> closed = true, onClose() 被调用, waiters 被清空
 22+-> 但 idleTimer 仍在运行
 23+-> idleTimer 触发 → fail() → markError() → closed 检查返回 false
 24+-> session 对象直到 timer 触发才能被 GC
 25+```
 26+
 27+## 根因
 28+
 29+`finishWithEvent()` 方法中只做了三件事:
 30+
 31+1. 设置 `closed = true`
 32+2. 入队最后一个事件或 resolve waiter
 33+3. 调用 `onClose(requestId)`
 34+
 35+没有清除 `openTimer` 和 `idleTimer`。
 36+
 37+相关代码位于 `FirefoxBridgeApiStreamSession` 的私有方法中:
 38+
 39+- `resetOpenTimer()` 设置 `this.openTimer = setTimeoutImpl(...)`
 40+- `resetIdleTimer()` 设置 `this.idleTimer = setTimeoutImpl(...)`
 41+- `finishWithEvent()` 没有调用 `clearTimeoutImpl()` 清除这两个 timer
 42+
 43+## 复现步骤
 44+
 45+```bash
 46+TOKEN="<BAA_SHARED_TOKEN>"
 47+
 48+# 发起一个 SSE 模式的 browser request
 49+curl -s https://conductor.makefile.so/v1/browser/request \
 50+  -X POST -H "Content-Type: application/json" \
 51+  -H "Authorization: Bearer $TOKEN" \
 52+  -d '{
 53+    "platform": "claude",
 54+    "path": "/api/organizations",
 55+    "method": "GET",
 56+    "responseMode": "sse"
 57+  }'
 58+
 59+# 请求正常完成后,session 的 idleTimer(默认 30s)仍在运行
 60+# 30s 后 timer 触发,尝试 fail() 但被 closed 保护
 61+# 这 30s 内 session 对象无法被 GC
 62+```
 63+
 64+## 当前影响
 65+
 66+- 不影响功能正确性(`closed` flag 做了保护)
 67+- 每个 stream session 关闭后最多多占用 30s(idleTimeoutMs)内存
 68+- 对于 openTimeout(10s),如果 stream 在 open 之前就关闭了,会多占 10s
 69+- 在高频 SSE 请求场景下,内存占用会有可观察的波动
 70+
 71+## 修复建议
 72+
 73+### 方案 A(推荐)
 74+
 75+在 `finishWithEvent()` 方法的开头清除所有 timer:
 76+
 77+```typescript
 78+private finishWithEvent(event: FirefoxBridgeStreamEvent): boolean {
 79+  // 清除所有待触发的 timer
 80+  if (this.openTimer != null) {
 81+    this.clearTimeoutImpl(this.openTimer);
 82+    this.openTimer = null;
 83+  }
 84+  if (this.idleTimer != null) {
 85+    this.clearTimeoutImpl(this.idleTimer);
 86+    this.idleTimer = null;
 87+  }
 88+
 89+  // 原有逻辑
 90+  this.closed = true;
 91+  // ...
 92+}
 93+```
 94+
 95+同时在 `cancel()` 方法中也加同样的清理(cancel 最终会调 fail -> markError -> finishWithEvent,所以只要 finishWithEvent 里做了就够,但显式清理更安全)。
 96+
 97+## 严重程度
 98+
 99+Low
100+
101+- 有 `closed` flag 保护,不会导致功能错误
102+- 影响仅为短暂内存延迟释放(最多 30s)
103+- 不会累积(timer 触发后对象即可被 GC)
104+
105+## 发现时间
106+
107+2026-03-26 by Claude (code review)
108+
109+## 备注
110+
111+- 修复非常简单,4 行代码
112+- 建议修复后补单元测试:创建 stream session → markEnd() → 验证 openTimer 和 idleTimer 都被清除
113+- 可以和 BUG-011、BUG-012 一起修,同一个 PR
A plans/discuss/DISCUSS-CODE-REVIEW.md
+273, -0
  1@@ -0,0 +1,273 @@
  2+# DISCUSS: 代码审查 — Bug、风险与优化建议
  3+
  4+日期:2026-03-26
  5+来源:Claude 审查 conductor-daemon、codexd、browser-request-policy、firefox-bridge、firefox-ws 全部 TypeScript 源码
  6+
  7+---
  8+
  9+## 进度评估
 10+
 11+代码成熟度相当高。T-S001~T-S020 的质量在代码层面是可验证的:
 12+
 13+- conductor-daemon 的 leader lease / heartbeat / scheduler 三件套完整,单节点自选举可用
 14+- codexd 的 app-server 模式(stdio transport)完整,session/turn 生命周期管理到位
 15+- firefox-bridge 的 SSE stream session 有 open/idle timeout、buffer overflow 保护、seq tracking,设计远超 MVP
 16+- browser-request-policy 的限流/抖动/退避/熔断全部实现且已参数化
 17+- 手写 WebSocket 帧编解码(firefox-ws.ts)避免了 ws 库依赖,轻量且正确
 18+
 19+总体评价:**可上线质量的 MVP**,以下发现的问题多数是边缘场景。
 20+
 21+---
 22+
 23+## Bug(需修复)
 24+
 25+### BUG-A: writeHttpResponse drain handler 可能永远不触发
 26+
 27+**文件**:`apps/conductor-daemon/src/index.ts`,`writeHttpResponse()` 函数
 28+
 29+**问题**:
 30+
 31+```typescript
 32+if (!writableResponse.write(payload.body)) {
 33+  await new Promise<void>((resolve) => {
 34+    writableResponse.on?.("drain", resolve);
 35+  });
 36+}
 37+```
 38+
 39+如果连接在 `write` 返回 false 之后、drain 触发之前被关闭(客户端断开),这个 Promise 永远不 resolve。结果:
 40+
 41+- 请求处理协程挂死
 42+- 对应的 HTTP server connection 对象无法被 GC
 43+- 如果并发请求多,会逐渐耗尽内存
 44+
 45+同样的模式在 streamBody 循环里也出现了一次。
 46+
 47+**修复建议**:
 48+
 49+```typescript
 50+await new Promise<void>((resolve) => {
 51+  const onDrain = () => { cleanup(); resolve(); };
 52+  const onClose = () => { cleanup(); resolve(); };
 53+  const cleanup = () => {
 54+    writableResponse.off?.("drain", onDrain);
 55+    writableResponse.off?.("close", onClose);
 56+  };
 57+  writableResponse.on?.("drain", onDrain);
 58+  writableResponse.on?.("close", onClose);
 59+});
 60+```
 61+
 62+**严重度**:Medium-High(生产环境慢客户端/代理断连时会触发)
 63+
 64+---
 65+
 66+### BUG-B: browser-request-policy waiter 无超时,可永久挂起
 67+
 68+**文件**:`apps/conductor-daemon/src/browser-request-policy.ts`
 69+
 70+**问题**:
 71+
 72+```typescript
 73+private async acquireTargetSlot(state: BrowserRequestTargetState): Promise<void> {
 74+  if (state.inFlight < this.config.concurrency.maxInFlightPerClientPlatform
 75+      && state.waiters.length === 0) {
 76+    state.inFlight += 1;
 77+    return;
 78+  }
 79+  await new Promise<void>((resolve) => {
 80+    state.waiters.push(resolve);
 81+  });
 82+}
 83+```
 84+
 85+如果持有 slot 的请求因为 Firefox 断连或 conductor 内部错误而没有调用 `lease.complete()`,这个 waiter Promise 永远不被 resolve。后续所有对同一 target 的请求都会卡死。
 86+
 87+`acquirePlatformAdmission` 有同样的问题。
 88+
 89+**修复建议**:
 90+
 91+1. 给 waiter 加超时(例如 120s),超时后 reject 并从 waiters 数组中移除
 92+2. 或者在 `BrowserRequestPolicyLeaseImpl` 的析构/finalize 路径中确保 `complete()` 一定被调用
 93+3. 在 `completeRequest` 的 `releaseTargetSlot` 中加防御:如果 `inFlight` 已经是 0 但 waiters 非空,说明 slot 泄漏,应主动释放一个 waiter
 94+
 95+**严重度**:Medium(当前 maxInFlightPerClientPlatform=1,一次泄漏就会永久卡死该 target)
 96+
 97+---
 98+
 99+### BUG-C: stream session openTimer/idleTimer 在 closed 后可能触发
100+
101+**文件**:`apps/conductor-daemon/src/firefox-bridge.ts`,`FirefoxBridgeApiStreamSession`
102+
103+**问题**:
104+
105+`resetOpenTimer` 和 `resetIdleTimer` 设置了 setTimeout,但 `finishWithEvent`(关闭 session)只把 `closed = true`,没有清除 openTimer 和 idleTimer。如果关闭后 timer 触发,会调用 `fail()` → `markError()` → 检查 `closed` 返回 false,看起来安全。但 `fail` 内部会再调 `markError` → `finishWithEvent`,其中 `onClose(requestId)` 会被重复调用(如果 timer 在 close 之后的事件循环 tick 中触发)。
106+
107+**实际影响**:较小,因为 `closed` flag 做了保护。但 timer 没清会导致 session 对象无法被 GC 直到 timer 触发。
108+
109+**修复建议**:在 `finishWithEvent` 中加:
110+
111+```typescript
112+if (this.openTimer) { this.clearTimeoutImpl(this.openTimer); this.openTimer = null; }
113+if (this.idleTimer) { this.clearTimeoutImpl(this.idleTimer); this.idleTimer = null; }
114+```
115+
116+**严重度**:Low(有 closed 保护,影响仅是短暂内存延迟释放)
117+
118+---
119+
120+## 风险(非 Bug 但需关注)
121+
122+### RISK-1: local-api.ts 5434 行,单文件过大
123+
124+**问题**:
125+
126+一个文件承载了全部 HTTP 路由处理(describe、browser、codex、host-ops、tasks、status),任何改动都需要在 5000+ 行中定位,merge conflict 概率高。
127+
128+**建议**:按 route group 拆分为:
129+
130+| 文件 | 职责 | 预估行数 |
131+|---|---|---|
132+| `local-api.ts` | 路由分发 + 共享工具 | ~500 |
133+| `routes/describe.ts` | describe 读面 | ~400 |
134+| `routes/browser.ts` | browser 系列 | ~1500 |
135+| `routes/codex.ts` | codex 系列 | ~300 |
136+| `routes/host-ops.ts` | exec/files | ~200 |
137+| `routes/tasks.ts` | tasks/runs | ~300 |
138+| `routes/system.ts` | system state/pause/resume | ~200 |
139+
140+**优先级**:Medium(不影响功能,但影响开发效率和 codex 任务分配)
141+
142+---
143+
144+### RISK-2: normalizeOptionalString 重复 7 次
145+
146+**文件**:分布在 conductor-daemon (4处)、codexd (1处)、status-api (1处)、codex-exec (1处)
147+
148+**问题**:
149+
150+同一个函数被复制了 7 次,且签名不完全一致:
151+- 4 处接受 `string | null | undefined`
152+- 2 处接受 `unknown`
153+- 1 处返回 failure 对象
154+
155+**建议**:提取到 `packages/shared-utils/` 或直接放在已有的 `packages/logging/` 中导出。
156+
157+**优先级**:Low(不影响功能,但增加维护负担)
158+
159+---
160+
161+### RISK-3: @ts-ignore 导入 status-api 构建产物
162+
163+**文件**:`apps/conductor-daemon/src/local-api.ts` 顶部
164+
165+```typescript
166+// @ts-ignore conductor reuses the built status-api snapshot normalizer directly.
167+import { createStatusSnapshotFromControlApiPayload } from "../../status-api/dist/...";
168+// @ts-ignore conductor reuses the built status-api HTML renderer directly.
169+import { renderStatusPage } from "../../status-api/dist/...";
170+```
171+
172+**问题**:
173+
174+- `@ts-ignore` 跳过了类型检查,如果 status-api 的导出接口变了,编译不会报错,运行时才崩
175+- conductor-daemon 构建依赖 status-api 已完成构建,增加了构建顺序耦合
176+- 这是 STATUS_SUMMARY 里提到的已知技术债,但还没有跟踪卡
177+
178+**建议**:
179+
180+1. 近期:把 `createStatusSnapshotFromControlApiPayload` 和 `renderStatusPage` 提取到 `packages/status-renderer/`
181+2. 远期:status-api 瘦身后直接合入 conductor-daemon
182+
183+**优先级**:Medium(已在 STATUS_SUMMARY 低优先级 backlog 中,但缺正式任务卡)
184+
185+---
186+
187+### RISK-4: codexd ensureAppServerClient 并发初始化窗口
188+
189+**文件**:`apps/codexd/src/daemon.ts`,`ensureAppServerClient()`
190+
191+**问题**:
192+
193+初始化失败后 `finally` 中设 `appServerInitializationPromise = null`,如果多个 caller 同时等待,失败后它们都会看到 null 并各自发起新的初始化。虽然功能上是 retry,但可能导致并发创建多个 transport 连接。
194+
195+**实际影响**:较小(当前 codexd 是串行使用的),但 task scheduler 上线后可能并发调用。
196+
197+**建议**:加一个 `initializationInProgress` lock 或 cool-down 计时器。
198+
199+**优先级**:Low(task scheduler 上线前不会触发)
200+
201+---
202+
203+### RISK-5: writeHttpResponse streamBody 循环无 abort 检查
204+
205+**文件**:`apps/conductor-daemon/src/index.ts`,`writeHttpResponse()`
206+
207+**问题**:
208+
209+```typescript
210+for await (const chunk of payload.streamBody) {
211+  if (writableResponse.destroyed === true) { break; }
212+  ...
213+}
214+```
215+
216+只检查了 `destroyed`,没检查传入的 `request.signal`。如果客户端取消请求但 socket 还没 destroy(比如 HTTP/2 RST_STREAM),async generator 会继续迭代直到自然结束。
217+
218+**实际影响**:较小(当前 SSE stream 有 idle timeout 保护)。
219+
220+**优先级**:Low
221+
222+---
223+
224+## 优化建议
225+
226+### OPT-1: browser-request-policy 的 Map 永远不清理
227+
228+`platforms` 和 `targets` 两个 Map 只增不删。如果长时间运行且 Firefox 重连时 clientId 变化,旧的 targetState 会累积。
229+
230+建议加一个定期清理(比如每小时清除 lastSuccessAt 超过 24h 且 inFlight=0 且 waiters=[] 的 target)。
231+
232+### OPT-2: conductor-daemon index.ts 也有 1300+ 行
233+
234+`ConductorDaemon` 和 `ConductorRuntime` 类 + CLI 解析 + 辅助函数都在一个文件里。可以拆分为:
235+
236+- `daemon.ts` — ConductorDaemon 类
237+- `runtime.ts` — ConductorRuntime 类
238+- `cli.ts` — CLI 解析和 main 入口
239+- `config.ts` — 配置解析和校验
240+
241+### OPT-3: 手写 WebSocket 帧编解码没有处理分片帧
242+
243+`firefox-ws.ts` 的帧解析只处理 FIN=1 的完整帧。如果 Firefox 插件发送分片帧(FIN=0),会被当作无效帧丢弃。当前可能不会触发(浏览器扩展通常不分片),但不符合 RFC 6455。
244+
245+---
246+
247+## 测试覆盖评估
248+
249+- conductor-daemon 有 `index.test.js`,覆盖了 daemon 生命周期和 fixture 清理
250+- codexd 有 `index.test.js`,覆盖了 session/turn 流程
251+- browser-request-policy 的限流/熔断逻辑**没有专门测试文件**
252+- firefox-bridge 的 stream session(最复杂的组件之一)**没有专门测试文件**
253+- firefox-ws 的帧编解码**没有专门测试文件**
254+
255+建议优先补 browser-request-policy 和 firefox-bridge 的单元测试。
256+
257+---
258+
259+## 总结
260+
261+| # | 类型 | 问题 | 严重度 | 行动 |
262+|---|---|---|---|---|
263+| BUG-A | Bug | drain handler 永久挂起 | Medium-High | 加 close 监听 |
264+| BUG-B | Bug | policy waiter 无超时 | Medium | 加超时或保证 complete |
265+| BUG-C | Bug | stream timer 未清除 | Low | finishWithEvent 里清 timer |
266+| RISK-1 | 风险 | local-api.ts 5434 行 | Medium | 拆分路由模块 |
267+| RISK-2 | 风险 | normalizeOptionalString x7 | Low | 提共享包 |
268+| RISK-3 | 风险 | @ts-ignore 导入 | Medium | 提共享包 |
269+| RISK-4 | 风险 | ensureAppServerClient 并发 | Low | 加 lock |
270+| RISK-5 | 风险 | stream 无 abort 检查 | Low | 检查 signal |
271+| OPT-1 | 优化 | policy Map 不清理 | Low | 定期清理 |
272+| OPT-2 | 优化 | index.ts 1300+ 行 | Low | 拆文件 |
273+| OPT-3 | 优化 | WS 不处理分片帧 | Low | 按需处理 |
274+| TEST | 测试 | policy/bridge/ws 缺测试 | Medium | 补单元测试 |