Files
sanguo_moziplus_v2/docs/design/v2.7.2-counter-lifecycle-fix.md
T
cfdaily 0d7425b88c
Deploy / ci (push) Waiting to run
Deploy / deploy (push) Blocked by required conditions
Deploy / notify-deploy-failure (push) Blocked by required conditions
auto-sync: 2026-06-07 01:35:53
2026-06-07 01:35:53 +08:00

8.1 KiB
Raw Blame History

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 输出格式:

{ "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 冷却(低优先级)