---
name: system-design-lessons
description: >-
  系统设计实战教训：Counter/锁生命周期、进程管理、续杯机制、
  广播路径一致性、JSON 解析验证。涉及并发控制、进程调度、
  外部命令集成时触发。
---

# 系统设计教训

> 来源：从三国团队（庞统/司马懿/诸葛亮）~2GB 对话历史中蒸馏
> 卡片数：7（批次2卡片4/5/6/7/8/9/14，部分与批次1卡片7有交叉）

## 适用场景

- 设计并发控制、锁、counter 机制时
- 实现进程管理（spawn/retry/exit）时
- 集成外部命令（stdout JSON 解析）时
- 实现广播/单播路径时
- 需要三层一致性审查（PRD→设计→代码）时

## 经验清单

### 1. Counter/锁的生命周期必须贯穿完整调用链（severity: high）

**场景**：Agent 并发控制需要 counter/锁机制。acquire-release 不成对会导致永久占满或双重释放。

**正确做法**：
1. **Counter 贯穿 retry 链**：acquire 在 spawn 前，release 在最终退出（done/failed）后
2. Retry 不释放 counter，而是复用同一个 counter slot
3. Cooldown 机制：release 后短期冷却，防止立即 re-acquire 导致抖动
4. 三层控制：cooldown → global limit → per-agent → per-session

**⚠️ 常见错误**：
- `_handle_exit` retry 路径提前调 `on_complete("retry_release")` 释放 counter，导致同一 Agent 被并发 spawn 多次 [simayi/mail]
- `_do_retry` 中 counter 仍被占用但 `on_complete=None`，续杯永远无法 acquire

**关键细节**：
- 所有 `release` 调用都要带 session_id（counter v2.1 的教训）
- on_complete 回调的时序：进程退出 ≠ 回调完成
- 广播 spawn 路径也要 catch AgentBusyError

**反面教训**：counter 提前释放导致 zhipu API 429（rate limit 被打爆），Gateway 假死。修复后 counter 泄漏导致续杯死循环。

> 来源：批次2卡片6（counter生命周期，freq=3）

---

### 2. 进程退出 ≠ 资源释放（severity: high）

**场景**：异步系统中，子进程退出和回调执行不是原子操作。假设进程退出时资源已释放会出现竞态。

**正确做法**：
1. 在 `_do_retry` 中手动释放 counter，并将 `on_complete` 设为 None 防止 double release
2. 资源释放应有明确触发点，不能依赖"进程退出自然释放"
3. 每条退出路径都要有对应的资源释放

**⚠️ 常见错误**：
- 进程退出 → on_complete 未调用 → counter 仍占用 → spawn_full_agent 的 can_acquire 返回 False → AgentBusyError → 续杯永远不成功

**关键细节**：
- `on_complete=None` 后续杯 spawn 完成无幻觉门控，靠 ticker 超时兜底——影响可接受但需记录
- 安装目录同步是必须的检查项（司马懿每次评审都验证）

**反面教训**：未处理回调时序导致续杯死循环，任务永远无法完成。

> 来源：批次2卡片7（进程管理，freq=2）

---

### 3. 续杯（retry）机制是最大假死来源（severity: high）

**场景**：任务执行超时后需要 retry。retry 机制本身有 Bug 会导致无限重试或永久卡住。

**正确做法**：
1. 续杯模板必须专用（MAIL_RETRY_PROMPT），明确禁止状态转换命令
2. retry_count 必须在每次续杯时递增，max_retries 兜底
3. 续杯应复用 session 保持上下文连续性
4. use_main_session 在三处（主 dispatch、legacy broadcast、retry）必须统一

**⚠️ 常见错误**：
- retry_count 在续杯中永远不变（retry_count=1/3），续杯永不停
- Mail 续杯用 RETRY_PROMPT（含 review 标记指令），Agent 标了 review 但 _mail_auto_complete 只认 done
- 续杯创建新 session 而非复用 main session，每次重试是全新上下文，丢失之前工作

**关键细节**：
- 续杯模板的"不要执行状态转换"指令是关键——Agent 容易自作主张标 done
- 司马懿在 Bug-8 评审中确认 use_main_session 必须统一

**反面教训**：续杯死循环阻塞 tick，导致整个调度停滞。PM2 restart 后旧 session 仍在续杯，新 session 又被创建，资源爆炸。

> 来源：批次2卡片8（续杯机制，freq=5+）

---

### 4. 广播路径与单播路径行为必须一致（severity: medium）

**场景**：任务调度有指定 Agent 的单播和广播认领两条路径。路径行为不一致会导致系统性问题。

**正确做法**：
1. 所有 spawn 路径（单播、广播、retry）必须传递完整上下文参数
2. 评审时重点检查：新功能是否覆盖了所有 spawn 路径
3. 广播 spawn 路径也要 catch AgentBusyError

**⚠️ 常见错误**：
- 广播路径 `ticker._broadcast_claim` 调 `spawn_full_agent` 时没传 `task_id` 和 `db_path`
- 导致续杯时无法查询任务状态，retry_count 永远不变 [simayi/mail]
- Mail 变广播导致所有 Agent 假死——路径不一致的典型案例

**关键细节**：
- 司马懿在 v2.7.2 Pipeline 重构评审中建议统一 TickResult 返回值 + 方法注册
- 每次新增参数时，必须检查所有 spawn 调用点

**反面教训**：广播路径缺失参数导致续杯死循环，影响所有通过广播认领的任务。

> 来源：批次2卡片9（广播路径，freq=3）

---

### 5. stdout JSON 解析路径必须实测验证（severity: medium）

**场景**：`openclaw agent --json` 的输出结构可能随版本变化。解析路径是猜的而非实测的会导致静默失败。

**正确做法**：
1. 解析外部命令输出前，先用实际命令验证输出格式
2. 评审时要求看到实测输出样例
3. JSON 解析失败时应该报错，不应静默走 fallback

**⚠️ 常见错误**：
- spawner 代码取 `data.response.meta.transport`，但实际输出格式不同
  > "P0：_parse_stdout_json 解析路径错误（根因）" [pangtong/mail]
- 解析不到数据时走了 fallback，掩盖了问题——静默失败比报错更危险

**关键细节**：
- 这个 Bug 从第一天就存在（"从第一天就存在的根因 bug"）
- 司马懿在 Spawner v3.0 评审中指出新路径 `data.result.meta.executionTrace` 需确认
- 所有依赖 stdout 判定的功能（如幻觉门控）都会受影响

**反面教训**：解析路径错误导致 stdout 信息丢失，幻觉门控等关键功能失效。

> 来源：批次2卡片14（JSON解析，freq=2）

---

### 6. 设计文档-代码三层一致性审查（severity: high）

**场景**：迭代开发中设计文档更新不及时，或代码实现偏离设计。需要 PRD→设计→代码的三层审查。

**正确做法**：
1. 每次代码评审前，先对设计文档逐条检查实现覆盖度
2. 设计文档用明确编号（§B1/B2/B3）便于逐条追踪
3. 安排"背靠背一致性审查"：PRD→设计→代码全覆盖
4. 司马懿的核心职责就是做这种三层对照

**⚠️ 常见错误**：
- 设计文档 §B2 在代码中完全遗漏，评审者初评也未发现 [pangtong/mail]
  > "代码只实现了 B1/B3/B4，B2 遗漏了"

**关键细节**：
- 庞统专门发 #311 邮件请求三层一致性审查
- 司马懿在 counter 评审中发现设计与代码不一致
- 三层审查顺序：先 PRD→设计（需求覆盖），再设计→代码（实现忠实）

**反面教训**：设计-代码不一致导致假死检测失效，compact 后无法区分"还在跑"和"卡死了"。

> 来源：批次2卡片4（设计-代码一致性，freq=4+）

---

### 7. E2E 测试必须用真实环境（severity: medium）

**场景**：单元测试只能验证单个模块逻辑。编排系统的 Bug 往往出在模块交互，只有真实环境 E2E 能暴露。

**正确做法**：
1. E2E 测试走真实 Ticker + 真实 Agent spawn，不手动推动状态
2. 测试用例覆盖完整生命周期：创建→调度→执行→续杯→完成/失败
3. 司马懿独立跑 E2E 并做根因分析，与庞统修复做交叉验证
4. 每次修复后全量重跑 E2E（不是只跑失败的用例）

**⚠️ 常见错误**：
- 只跑单元测试就部署，dashboard 上大量任务停在中间状态 [user/simayi-correction]
- 手动改 DB 推进任务（污染数据、掩盖真正 bug）

**关键细节**：
- 司马懿 E2E 报告格式：通过/失败计数 + 每个失败用例的根因分析
- 从 6/10 → 8/10 → 9/10 → 10/10 的渐进修复证明 E2E 驱动开发有效
- MEMORY.md 中 L1-L7 经验教训也是 E2E 驱动复盘的产出

**反面教训**：未做 E2E 导致假死问题在生产环境中才暴露，排查成本远高于测试阶段。

> 来源：批次2卡片10（E2E测试，freq=3）+ MEMORY.md L1-L7

---

## 检查清单（快速参考）

- [ ] Counter 的 acquire-release 是否成对？retry 链中是否贯穿？
- [ ] 进程退出后的 on_complete 回调是否处理？有无资源泄漏？
- [ ] 续杯模板是否专用？是否禁止状态转换指令？
- [ ] retry_count 是否在每次续杯时递增？
- [ ] 续杯是否复用 session（不创建新 session）？
- [ ] 新增参数时，所有 spawn 路径（单播/广播/retry）是否都传了？
- [ ] 外部命令输出格式是否实测验证过？
- [ ] JSON 解析失败是报错还是静默 fallback？
- [ ] 设计文档是否逐条检查了实现覆盖度？
- [ ] E2E 测试是否走真实环境（非手动推进状态）？
