Files
sanguo_moziplus_v2/docs/design/archive-2.0/v2.7.2-counter-lifecycle-fix.md
T
2026-05-28 08:45:47 +08:00

174 lines
8.1 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.
# v2.7.2 防重复调用 & 防无限续杯 — 完整方案
**版本**: v2.0
**日期**: 2026-05-26
**作者**: 庞统
**状态**: 已实现 + 待最终评审
---
## 1. 核心原则
> **每次 agent 调用都是独占的。** openclaw 无论成功失败都会返回,最差情况 timeout 返回。谁占用谁持有,进程退出就 release。
---
## 2. 根因分析
### 2.1 2026-05-25/26 事件复盘
司马懿 + 庞统被 moziplus-v2 连续 spawn,叠加 API 调用触发 zhipu 429,双模型不可用导致 Gateway 假死。
### 2.2 发现的三个根因
| 根因 | 影响 | 严重度 |
|------|------|--------|
| **P0`_parse_stdout_json` 解析路径错误** | `data.get("meta")` 应为 `data["response"]["meta"]`。68% 的 spawn 结果 transport=null62/91 次)。A 场景分类全部失效 | **致命** |
| counter 生命周期是任务级 | retry 绕过 dispatcher 直接 spawn,不检查 counter | 高 |
| spawn 前缺 session state 检查 | webchat 占用 main session 时仍然 spawn,注定失败 | 高 |
### 2.3 P0 详细说明
openclaw agent `--json` 输出格式:
```json
{ "kind": "agent-response", "response": { "meta": { "transport": "gateway", ... } } }
```
代码取的是 `data.get("meta")` 而不是 `data["response"]["meta"]`,导致:
- 所有 transport=null → A1/A5/A6 分类失效
- session lock 阻塞退出被误判为 gateway_timeoutA2/A3)→ 续杯循环
- 续杯循环叠加 API 调用 → 429 → Gateway 假死
---
## 3. 完整场景清单
### 3.1 Spawn 前检查(拦截无效 spawn
| # | 场景 | 检测方法 | 检测到后方案 |
|---|------|---------|-------------|
| L1 | moziplus 内部并发 | counter.can_acquire() | AgentBusyError → 等 ticker |
| L2 | API 429 冷却期 | counter.is_cooling_down() | AgentBusyError → 等 ticker |
| L3a | main session 被外部占用 | _check_session_state → lock_pid_alive | AgentBusyError → 等 ticker |
| L3b | main session 正在执行 | _check_session_state → status=processing | AgentBusyError → 等 ticker |
| L3c | main session 正在 compact | _check_session_state → recent_compact | AgentBusyError → 等 ticker |
L1-L3 统一结果:不 spawn,任务保持 workingticker 30 秒后重新调度。
L1+L3 互补:counter 防 moziplus 内部并发,session state 防外部占用(webchat/Control UI/cron)。
### 3.2 Spawn 后 — 进程退出(情况 A,0-630 秒内)
| # | 场景 | 检测方法 | 条件 | 检测到后方案 |
|---|------|---------|------|-------------|
| A1 | 正常完成 | stdout transport + 任务状态 | exit=0 + transport≠embedded + 终态 | release counter → 结束 |
| A2/A3 | Gateway timeout | stdout transport + 任务状态 | exit=0 + 非终态 + transport 正常 | release counter → 续杯 |
| A4 | Agent 自标 failed | 任务状态 | 任务=failed | release counter → 结束 |
| A5/A6 | Gateway fallback | stdout transport=embedded + fallbackReason | exit=0 + transport=embedded | release counter → 标 failed + escalate |
| A7 | 认证失败 | exit≠0 + stderr | exit≠0 + stderr 含 401/403 | release counter → 标 failed + escalate |
| A8 | Gateway 不可达 | exit≠0 + stderr | exit≠0 + stderr 含 ECONNREFUSED/ETIMEDOUT | release counter → 等 ticker |
| A9 | API 429 | exit≠0 + stderr | exit≠0 + stderr 含 rate_limit/500/503 | release counter → 推回 pending + 冷却 120s |
| A10 | Compact 失败 | exit≠0 + stderr | exit≠0 + stderr 含 compaction-diag | release counter → 等 ticker |
| A11 | Session lock 冲突 | exit≠0 + stderr | exit≠0 + stderr 含 lock/busy/concurrent | release counter → 等 ticker |
| A12 | 兜底未知错误 | 兜底 | exit≠0 不匹配以上 | release counter → 等 ticker |
| A2+P2 | transport=null 兜底 | stderr 辅助判断 | exit=0 + transport=null + 非终态 | 检查 stderr → lock/compact/api_error 或走 gateway_timeout |
**续杯:只有 A2/A3 才触发续杯。其他都 release counter,等 ticker 或推回 pending。**
### 3.3 Spawn 后 — 进程不退出(情况 B,630 秒后)
| # | 场景 | 检测方法 | 条件 | 检测到后方案 |
|---|------|---------|------|-------------|
| B1 | 假死(lock PID 死了) | _check_session_state | lock_pid 存在但 PID 不存活 | 标 failed + escalate + release counter |
| B2 | Compact 进行中 | _check_session_state + stderr | lock_pid 存活 + stderr 有 compact | 继续等(不递增计数,独立上限) |
| B3 | 进程不退出 | _check_session_state | lock_pid 存活 + 无 compact | 继续等(递增计数,≥3 次 → failed) |
| B4 | Session 状态不匹配 | _check_session_state | sessions.json status≠running | 等 60s → 按 B3 处理 |
### 3.4 续杯限制
| # | 计数器 | 上限 | 超限后方案 |
|---|--------|------|-----------|
| R1 | retry_count | 3 次(每次 ~10 分钟) | 标 failed + escalate |
| R2 | connect_retry_count | 3 次 | 标 failed |
| R3 | api_retry_count | 3 次 | 标 failed |
| R4 | lock_retry_count | 3 次 | 标 failed |
| R5 | monitor_timeout_count | 3 次 | 标 failed |
### 3.5 Ticker 兜底(每 30 秒检查)
| # | 检查 | 方案 |
|---|------|------|
| T1 | 进程存活性:counter 占用但 PID 不存活 | release counter + 推回 pending |
| T2 | 任务超时:working 超时(默认 30 分钟) | 标 failed |
### 3.6 假死复活术(2026-05-04 经验)
**现象**sessions.json 状态为 running 但 agent 长时间无响应。
**根因**Gateway 认为 session 还活着(running)但实际连接已断开,无超时清理。
**复活步骤**
1. 修改 sessions.json,把对应 session 的 status 从 running 改为 idle
2. 发心跳激活(把当前任务再发一遍给 agent)
**待设计**:将此复活术集成到 ticker `_check_timeouts` 中,检测到 sessions.json status=running 但 lock PID 不存活时自动执行。
---
## 4. 改动文件清单
| 文件 | 改动 | 版本 |
|------|------|------|
| `counter.py` | 新增 cooldown 机制(is_cooling_down / set_cooldown | v1 |
| `spawner.py` | 新增 AgentBusyError | v1 |
| `spawner.py` | spawn_full_agent 内部 counter acquire/release + wrapped_on_completetry/finally | v1 |
| `spawner.py` | spawn_full_agent 加 L3 session state 检查 | v2 |
| `spawner.py` | _classify_outcome 去掉 release_counter,只有 A2/A3 触发 retry | v1 |
| `spawner.py` | _classify_outcome 加 P2 transport=null 兜底 | v2 |
| `spawner.py` | _handle_exitA9 推回 pending + 冷却,A5/A6 标 failed + context 日志 | v1 |
| `spawner.py` | _do_retry 手动 release counter + 通过 spawn_full_agent 重试 | v1 |
| `spawner.py` | P0_parse_stdout_json 改为 data["response"]["meta"] | v2 |
| `spawner.py` | 新增 get_session_by_agent(进程存活性检查用) | v1 |
| `dispatcher.py` | 去掉 counter acquire/releaseon_complete 只含业务逻辑 | v1 |
| `ticker.py` | _spawn_available_agents 不再管 counter | v1 |
| `ticker.py` | _check_timeouts 加进程存活性检查 + 推回 pending | v1 |
| `ticker.py` | 新增 _is_pid_alive | v1 |
| `main.py` | counter 创建提前,传给 spawner | v1 |
---
## 5. 司马懿评审记录
### 第一次评审(mail-1779726169654
| 评审意见 | 结论 | 理由 |
|---------|------|------|
| wrapped_on_complete 加 try/finally | ✅ 采纳 | 防御性编程 |
| A5/A6 加 context 日志 | ✅ 采纳 | 排查方便 |
| per-provider 冷却 | ⏭ 延后 | 低优先级 |
| crash_count per-agent 累计,禁用 agent | ❌ 不采纳 | 崩溃可能是任务问题不是 agent 问题 |
| can_acquire 失败推回 claimed | ❌ 不采纳 | asyncio 单线程无竞态 |
| release 和 acquire 之间有竞态窗口 | ❌ 不存在 | 内存同步操作 |
### 第二次评审(_do_retry counter 时序)
| 评审意见 | 结论 |
|---------|------|
| _do_retry 续杯退化为 AgentBusyError | ✅ 采纳:_do_retry 入口手动 release counter |
### 第三次评审(P0/P1/P2 补丁)
| 评审意见 | 结论 |
|---------|------|
| P0 stdout 解析路径修复 | 待评审 |
| P1 spawn 前检查 | 待评审 |
| P2 transport=null 兜底 | 待评审 |
---
## 6. 待办
- [ ] 假死复活术集成到 ticker
- [ ] 历史数据清理(P0 修复前 62 条 transport=null 的 task_attempts
- [ ] per-provider 冷却(低优先级)