---
name: trial-and-error-patterns
description: >-
  从任务失败和重试中提炼的可复用模式：Counter/锁生命周期、进程管理、续杯机制、广播路径、JSON解析验证、中断恢复。
  在涉及并发控制、任务重启、系统资源管理的复杂任务时触发。
---

# Trial and Error Patterns

> 来源：从三国团队对话历史中的 226 次试错和 200 次成功中蒸馏（批次1-2）

## 适用场景

- 并发控制任务（counter/锁）
- 任务重启/续杯（retry）机制
- 进程管理（启动/退出/资源释放）
- 多路径调度（广播 vs 单播）
- 外部命令输出解析
- Agent 中断恢复与自检

## 模式清单

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

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

**错误做法**：
retry 路径提前调 `on_complete("retry_release")` 释放 counter，但续杯 spawn 时 counter 已释放，导致同一 Agent 被并发 spawn 多次。另一条路径中 `_do_retry` 的 on_complete 设为 None，counter 仍被占用但无法释放，续杯永远无法 acquire。 [批次2卡片6]

**正确做法**：
1. Counter 贯穿 retry 链：acquire 在 spawn 前，release 在最终退出（done/failed）后
2. Retry 不释放 counter，而是复用同一个 counter slot
3. 所有 `release` 调用都要带 session_id
4. release 后短期 cooldown，防止立即 re-acquire 导致抖动
5. 广播 spawn 路径也要 catch AgentBusyError

**关键细节**：
- 司马懿建议的四层控制：cooldown → global limit → per-agent → per-session
- counter 提前释放曾导致 zhipu API 429（rate limit 被打爆），Gateway 假死

---

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

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

**错误做法**：
spawner 在 retry 时：进程退出 → on_complete 未调用 → counter 仍占用 → can_acquire 返回 False → AgentBusyError → 续杯永远不会成功。 [批次2卡片7]

**正确做法**：
1. 资源释放应该有明确的触发点，不能依赖"进程退出自然释放"
2. 在 retry 入口手动释放 counter，并将 on_complete 设为 None 防止 double release
3. 审查每条退出路径（正常退出、异常退出、超时退出、retry 退出）是否都有对应的资源释放
4. on_complete=None 后需要替代兜底机制（如 ticker 超时），并记录为已知 trade-off

**关键细节**：
- 这类竞态条件在单元测试中难以复现，只有 E2E 真实环境才能暴露

---

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

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

**错误做法**：
多个 Bug 叠加导致续杯死循环：
1. retry_count 在续杯中永远不变（retry_count=1/3），续杯永不停
2. Mail 续杯用 RETRY_PROMPT（含 review 标记指令），Agent 标了 review，但 auto_complete 只认 done
3. 续杯创建新 session 而非复用 main session，每次重试丢失之前工作 [批次2卡片8]

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

**关键细节**：
- 续杯模板的"不要执行状态转换"指令是关键——Agent 容易自作主张标 done
- 续杯死循环会阻塞 tick，导致整个调度停滞

---

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

**场景**：任务调度有两条路径：指定 Agent 的单播和广播认领。如果两条路径的行为不一致，某些任务类型会系统性地出问题。

**错误做法**：
广播路径 `ticker._broadcast_claim` 调 `spawn_full_agent` 时没传 `task_id` 和 `db_path`，导致续杯时无法查询任务状态，retry_count 永远不变。 [批次2卡片9]

**正确做法**：
1. 所有 spawn 路径（单播、广播、retry）必须传递完整的上下文参数
2. 新增功能时逐条检查是否覆盖了所有 spawn 路径，不能只改单播忘了广播
3. 统一 TickResult 返回值 + 方法注册，避免路径分歧

**关键细节**：
- 状态机枚举值不一致是同类问题——SQL 查询用 `'executing'` 但实际枚举是 `'working'`，重启恢复时 working 节点被遗漏 [批次2卡片2]
- 安装目录同步（auto-sync）可能静默修改配置，需要与设计文档对照

---

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

**场景**：外部命令的输出结构可能随版本变化。如果解析路径是猜的而非实测的，会导致静默失败。

**错误做法**：
spawner 代码取 `data.response.meta.transport`，但实际 `openclaw agent --json` 的输出是 `{ response: { meta: { transport, ... } } }`，路径不对。解析不到数据时走了 fallback，掩盖了问题。这个 Bug 从第一天就存在。 [批次2卡片14]

**正确做法**：
1. 解析外部命令输出前，先用实际命令验证输出格式
2. 评审时要求看到实测输出样例，不接受"应该是这样"
3. 解析失败时显式报错或日志警告，不要静默 fallback

**关键细节**：
- 静默失败比报错更危险——fallback 掩盖了问题，导致所有依赖 stdout 判定的功能（如幻觉门控）静默失效
- 外部 CLI 的 `--json` 输出格式属于外部接口，需要像对待 API 一样验证

---

### 模式 6：中断恢复后必须自检（severity: medium）

**场景**：Agent 在执行过程中出现中断（进程挂起、网络断开、超时等），恢复后继续工作，但未检查中断期间发生了什么。

**错误做法**：
Agent 在设计评审完成后开始编码时"死掉"了 40 分钟，恢复后直接继续编码，没有自查中断原因、没有检查中断期间是否有新的指令或状态变化。 [批次1卡片10]

**正确做法**：
1. 发现自己有中断时，主动检查中断原因和时长
2. 检查中断期间是否错过了重要事件（新邮件、状态变更、用户指令）
3. 向用户汇报中断情况和恢复状态
4. 如果中断导致上下文丢失，回退到最近已确认的状态重新开始

**关键细节**：
- Agent 不一定能感知自己的中断，但可以通过时间戳、日志断点来推断
- 中断可能影响依赖链上的其他 agent——任务遗漏或重复执行

---

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

并发控制：
- [ ] Counter 的 acquire 和 release 是否成对？
- [ ] 所有 release 调用是否带 session_id？
- [ ] 续杯路径是否复用 counter slot 而非释放再获取？

Retry/续杯：
- [ ] retry_count 是否每次递增？max_retries 是否兜底？
- [ ] 续杯模板是否明确禁止状态转换？
- [ ] 是否复用 main session 而非创建新 session？
- [ ] 进程退出后资源是否有明确的释放触发点？

路径一致性：
- [ ] 广播路径和单播路径是否传递相同的完整参数？
- [ ] 新增功能是否覆盖了所有 spawn 路径？

外部接口：
- [ ] stdout JSON 解析路径是否用实际命令验证过？
- [ ] 解析失败时是否显式报错而非静默 fallback？

中断恢复：
- [ ] 中断恢复后是否检查了中断期间的事件？
- [ ] 是否向用户汇报了中断情况？
