Files
AgentCompany/docs/proposals/M12-renderer-state-audit.md
T

253 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Proposal M12: renderer 状态层审查(IPC 契约化后做)
> 来源:问题.md M12(行 290-294)、附录 D(行 403-408)
> 状态:proposed
> 提出:Roundtable 2026-04-20(Linus + Karpathy + Steve Jobs)
> Owner: TBD
> 估计工时:3-4 周
> Blocking dependencies:**M2(IPC 契约化)必须先完成**
## 问题陈述
104 个 IPC channel(见 M2)基本都在给 renderer 推状态:
- profile snapshot
- task list
- run events
- browser actions
- approvals
- messages
- …
renderer 如何 debounce、merge、避免 stale read,**本次 review 未深入**——但缺乏契约层的前提下几乎必然有一致性 bug。
典型问题猜测(需 M2 完成后验证):
1. **Race condition**:两个 IPC response 几乎同时到达,后到覆盖先到,但语义上应该合并
2. **Stale read**:renderer 发 request → main 返回 → 中间 main 已经又写了一次 → renderer 拿到 stale snapshot
3. **过度 refetch**:renderer 没有合适的 cache,每次切换页面都重 fetch(浪费 IPC round trip)
4. **Event dedup**:同一个 run event 被多个订阅路径接收,渲染时重复展示
5. **Optimistic update 回滚**:renderer 本地 optimistic 更新,但 main 拒绝后没有正确回滚
## 根因分析
当前未知——本 proposal 承认**review 没覆盖 renderer**。
Linus 视角推测:104 个 channel × N 种 payload shape × 若干订阅者 = 巨大的状态空间。没有类型契约(M2 未完成)时,renderer 层的一致性问题**无法静态证明也无法系统性测试**。M2 是前置条件——契约化后才能逐个 channel audit。
Karpathy 视角补充:renderer 的 UI state 错乱直接让用户看到 "agent 跑着但 UI 显示闲置" 或 "approval 已批但列表还在" 等典型 bug。这种 bug 很难用 unit test 捕捉,需要 e2e 验证 + state invariant checking。
## 设计方案
本 proposal 是**审查方案**而不是修复方案——具体修法要等审查结果。
### 审查范围
`src/renderer/` 全部代码,重点:
1. **状态管理库**:目前是 zustand(从 `package.json` 看到依赖)
2. **IPC 订阅点**:`useEffect` 里调 `window.api.invoke` / 订阅 event 的位置
3. **数据合并逻辑**:event 到达时如何 merge 到 store
4. **缓存策略**:哪些数据缓存、TTL 多少、如何失效
5. **Optimistic update**:哪些 action 先改 UI 再等 ack
### 审查 checklist
| 项 | 检查内容 |
|----|---------|
| 1 | 每个 zustand store 的 state shape 是否 type-safe(借助 M2 的 IpcOutput 类型) |
| 2 | 每个 IPC invoke 的 response 处理,是否考虑 race(后到请求覆盖先到请求) |
| 3 | 每个 event subscription,是否 dedup(同一 event 不同路径接收) |
| 4 | Optimistic update 的回滚逻辑完备性 |
| 5 | 组件 unmount 时 subscription cleanup |
| 6 | 跨 route 切换时 store 是否 reset(避免显示上一个 company 的数据) |
| 7 | 长列表的 virtualization(react-virtual 已在依赖里,实际使用覆盖度) |
| 8 | Loading / error / empty 三种边界态覆盖 |
### 预期产出
审查结束后产出 `docs/proposals/M12-renderer-findings.md`,按严重度列出发现的具体问题,每条给修复 proposal(可能演化为独立 proposal)。
### 审查方法
1. **静态**:
- grep `window.api.invoke` 找所有 call site
- grep `useEffect` + IPC 调用 找订阅点
- 对照 M2 的 IpcChannels 一一核对 shape
2. **动态**:
- 启动 app,开 React DevTools 看 zustand store 状态
- 触发典型场景(agent run、approval flow、task update),观察 state transition
- 故意制造 race(背景 run 发 event + 前景用户交互)
3. **e2e**:
- 用 Playwright(tests/e2e 已配置)写 invariant 检查脚本:
- 任何时刻 UI 显示的 "pending approvals" 数量 = main 进程 DB 查询结果
- 任何时刻 UI 显示的 "active runs" = DB runs where status=running
## Migration 计划(审查后)
审查阶段:
1. **Phase 1(1 周)**:静态扫描 + 形成 findings 列表
2. **Phase 2(1 周)**:动态测试 + Playwright invariant script
3. **Phase 3(1 周)**:按严重度归类,高严重度的 findings 进入后续 sprint 修复
4. **Phase 4(1-2 周)**:根据 findings 可能派生出新的 proposal(例如"renderer-state-race-fixes")
## 风险与回滚
| 风险 | 触发条件 | 回滚方法 |
|------|---------|---------|
| 审查范围失控 | 发现问题太多,sprint 无法覆盖 | 按严重度分批,每个 sprint 处理一类 |
| 修复引入新 bug | 动 renderer state 逻辑 | 每类修复独立 PR + e2e 验证 |
| M2 没完成就启动 | 契约化未完成导致审查无类型依据 | 不做,等 M2 |
## 验证标准(审查阶段)
1. **覆盖率**:grep 可见所有 `window.api.invoke` 都被审查
2. **Findings 数**:产出具体 finding 列表(预估 20-40 条)
3. **E2E invariant**:至少 5 条关键 invariant 被 playwright 脚本覆盖
4. **修复 backlog**:高优先 finding 进入下个 sprint
## 不在范围
- **不做 renderer 架构重写**:只是审查 + 针对性修复
- **不换状态管理库**:zustand 保留
- **不改 UI 设计**:只审数据流
- **不重构 route 层**:url routing 逻辑独立
- **不做 A/B 测试设施**:UI 实验独立 proposal
## 参考文件清单
- `src/renderer/`(全部,待审查)
- `src/preload/index.ts`(IPC bridge)
- `src/main/ipc-*.ts`(main 侧 handler)
- `tests/e2e/playwright.config.ts`(e2e 测试配置)
- M2 契约表(前置依赖)
## 附:为什么现在放一个"审查 proposal" 而不是修复 proposal
本次代码 review(2026-04-20)**明确跳过了 renderer 深入**——review report 也坦白了这点。列 M12 不是占位,是承认"这里有未知的坑"。
**第一性原理**:不能在没契约(M2)的前提下审 renderer 状态——审的结论无法落成 executable 测试。必须等 M2 完成后,renderer 层的问题才有具体形状。
M12 也是 IPC-related 所有 proposal 的收官——M2 完成 + M12 审查完 + 修复完,整个 main ↔ renderer 的契约面才算治理完整。
Karpathy 视角:Agent 的 UI 呈现错误("agent 跑完了但还在 spinner")直接伤害产品 perception——用户不会分辨是 LLM bug 还是 state bug,一律认为"这 agent 不可靠"。所以 renderer state 治理的优先级实际比列 M 级更高,但依赖链强迫它排在后面。
## 附:审查输出模板
审查结束后 `docs/proposals/M12-renderer-findings.md` 的结构:
```markdown
# M12 Findings (<date>)
## 🔴 Critical findings
### F-01: <具体问题>
- 文件:src/renderer/xxx.tsx:<行号>
- 重现:<最小重现步骤>
- 影响:<用户层面影响>
- 建议修复:<简短描述>
## 🟠 Major findings
...
## 🟡 Minor findings
...
## Covered e2e invariants
1. <invariant 1>
2. <invariant 2>
...
```
## 附:gating matrix
| Gate | 条件 | 放行判断人 |
|------|------|----------|
| G1 | M2 IPC 契约化完成 | Linus |
| G2 | 审查 checklist 8 项全覆盖 | Owner |
| G3 | Findings 列表产出并分类(≥20 条) | Linus |
| G4 | e2e invariant 脚本 ≥5 条可运行 | Karpathy |
| G5 | 高优先 findings 进入下 sprint 修复 | Owner |
## 附:关键 invariant 候选
e2e playwright 要覆盖的状态不变式:
1. UI "pending approvals" count = DB approvals where status = 'pending'
2. UI "active runs" = DB runs where status = 'running'
3. Task card 显示的 phase = DB tasks.phase(B2 完成后)
4. Agent message 未读数 = DB agent_message_reads 未读行数
5. 某 company 被切换后,UI 不应残留前一 company 数据
6. Approval 批准后,UI 应在 1s 内从 pending 列表消失
7. Task 从 todo → in_progress 时,spinner 应立即显示
8. Browser action 执行中,main 进程可见 subprocess status = running
## 附:为什么现在不做 proposal 只做审查
当前 review 团队(Linus + Karpathy + Jobs)**没有覆盖 renderer**——贸然给出修复 proposal 等于凭想象出方案。本 proposal 诚实承认这是"审查准备"阶段,具体修复待发现。
## 附:为什么不选其他方案
**方案 A(当前 proposal)**:先审查后修复,M2 做前置。
**方案 B(一次性 renderer 重写)**:风险极大——renderer 代码量不小,重写周期长,期间 UI 完全不可用。
**方案 C(按路由逐 page 重写)**:逻辑上可行,但 findings 可能跨 page(例如多个 page 用同一个 bug store),按 page 重写解决不了。
方案 A 是发现问题 → 针对性修复的稳妥路径。
## 附:常见 renderer state 反模式(待审查时排查)
审查重点观察以下 pattern:
1. **useEffect 里 invoke 没 AbortController**:组件 unmount 但 promise 仍 resolve,导致 "setState on unmounted component"
2. **event listener 没 cleanup**:`ipcRenderer.on("xxx", h)` 返回的 listener 没在 `useEffect` 清理
3. **派生 state 存 store**:本可以 derive 的数据写入 store,引入一致性问题
4. **Optimistic update 没 rollback**:`store.setState({ ... })` 后 invoke 失败,UI 保持错误态
5. **同一 data 多次 fetch**:切换 tab 每次重 fetch,没有 cache 或 cache 失效策略不当
6. **Route 切换 store 不 reset**:上一个 company 的数据残留到当前 company 视图
7. **Event dedup 缺失**:同一 run_event 被多个路径转发到 renderer,渲染时重复
8. **List 未 virtualize**:大列表渲染全量 DOM,滚动卡顿
审查时对每个 store 和每个大组件逐项核查。
## 附:playwright e2e invariant 脚本骨架
```ts
// tests/e2e/invariants.spec.ts(M12 产出)
test("pending approvals UI = DB", async ({ page, electronMain }) => {
await page.goto("/approvals");
const uiCount = await page.locator("[data-testid=pending-approval]").count();
const dbCount = await electronMain.evaluate(() =>
db.approvals.count({ status: "pending" })
);
expect(uiCount).toBe(dbCount);
});
test("active runs UI = DB", async ({ page, electronMain }) => {
await page.goto("/runs");
const uiRunning = await page.locator("[data-status=running]").count();
const dbRunning = await electronMain.evaluate(() =>
db.runs.count({ status: "running" })
);
expect(uiRunning).toBe(dbRunning);
});
// ... 至少 5 条 invariant
```
## 附:renderer store 梳理(待 M2 完成后形成清单)
M2 完成后,M12 审查第一步:把每个 zustand store 列出来 + 它订阅的 IPC channel 映射,形成:
| Store | 订阅 channel | 更新策略 | 发现的问题 |
|-------|------------|---------|----------|
| agentsStore | `agents/list` + `agents/updated` event | ? | 待审 |
| tasksStore | `tasks/list` + `tasks/updated` event | ? | 待审 |
| ... | ... | ... | ... |
## 附:e2e harness 运行频率
- PR 触发:快速 invariant(< 30s 跑完的子集)
- 夜间 build:全量 invariant + soak test(5 分钟持续交互)
- Release gate:全量 + manual QA pass
M12 审查结束后产出的 invariant 脚本会同时塞进这三层。