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

11 KiB
Raw Blame History

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 的结构:

# 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 脚本骨架

// 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 脚本会同时塞进这三层。