auto-sync: 2026-05-26 11:29:30

This commit is contained in:
cfdaily
2026-05-26 11:29:30 +08:00
parent 30cc27fa46
commit f29a194c9c
+106 -70
View File
@@ -1,6 +1,6 @@
# Spawner Monitor 设计文档
> 版本:v1.1 | 日期:2026-05-22 | 作者:庞统 | 状态:评审通过(v1.1 已修正评审意见
> 版本:v2.0 | 日期:2026-05-26 | 作者:庞统 | 状态:v2.0 大幅更新(P0 stdout 修复 + spawn 前检查 + counter 调用级 + 假死复活术
## 1. 背景与问题
@@ -15,13 +15,29 @@
## 2. 核心设计原则
1. **不主动 kill 进程**:进程可能还在正常执行,kill 会丢失所有进度
2. **保持 session 不变**:重试时复用同一个 session-id,Agent 能看到之前的上下文
3. **monitor timeout > Gateway timeout**:给 Gateway 足够时间自行处理
4. **续杯机制**:进程退出但任务未完成 → 用同一 session 再 spawn 一次
5. **escalate 不自动 kill**:超过重试上限后标记 failed + escalate,是否 kill 留给用户决定
1. **每次 agent 调用都是独占的**:openclaw 无论成功失败都会返回,最差情况 timeout 返回。谁占用谁持有,进程退出就 release
2. **counter 生命周期是调用级**spawn 时 acquire,进程退出就 release。不是任务级
3. **spawn 前检查所有可用信号**counter + 冷却期 + session statelock/processing/compact),避免注定失败的 spawn
4. **不主动 kill 进程**:进程可能还在正常执行,kill 会丢失所有进度
5. **续杯只有 Gateway timeout 才触发**lock/compact/api_error 等不续杯,等 ticker
6. **escalate 不自动 kill**:超过重试上限后标记 failed + escalate,是否 kill 留给用户决定
## 3. 参数配置
## 3. Spawn 前检查(拦截无效 spawn
spawn_full_agent 启动进程前,依次检查:
| # | 场景 | 检测方法 | 检测到后方案 |
|---|------|---------|-------------|
| 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 互补:counter 防 moziplus 内部并发,session state 防外部占用(webchat/Control UI/cron)。
所有检查失败统一走 AgentBusyError → 任务保持 working → ticker 30 秒后重新调度。
## 4. 参数配置
```yaml
daemon:
@@ -31,7 +47,7 @@ daemon:
max_monitor_timeouts: 3 # monitor timeout 上限(情况 B
```
## 4. 情况 A:进程在 monitor_timeout 内退出
## 5. 情况 A:进程在 monitor_timeout 内退出
```
0s 600s 630s
@@ -42,6 +58,21 @@ daemon:
进程退出后,读 stdout JSON + stderr + 查任务实际 API 状态。
### ⚠️ P0 修复:stdout JSON 解析路径
openclaw agent `--json` 输出格式是 `{ "response": { "meta": { ... } } }`
不是 `{ "meta": { ... } }``_parse_stdout_json` 必须取 `data["response"]["meta"]`
修复前 68% 的 spawn 结果 transport=null62/91 次),A 场景分类全部失效。
### ⚠️ P2 兜底:transport=null 时的 stderr 辅助判断
如果 P0 修复后 transport 仍为 null(解析失败),A2/A3 判定前先检查 stderr
- stderr 含 lock/busy → lock_conflict
- stderr 含 compact → compact_failed
- stderr 含 rate_limit → api_error
- 否则走 gateway_timeout(续杯)
### A1exit=0 + transport=gateway + 任务已是 done/review
```
@@ -55,7 +86,7 @@ daemon:
处理:
- 记录 outcome = "completed"
- counter.release()
- counter.release()(由 wrapped_on_complete 保证)
- 无需其他操作
```
@@ -64,16 +95,17 @@ daemon:
```
现象:
- exit_code = 0
- meta.transport = "gateway"
- meta.transport = "gateway"P0 修复后可正确解析)
- 任务 API 状态 = working
原因:Gateway timeout 触发,Agent 被中断但之前已写了 working。
Agent 可能执行了步骤1-2(working + 执行),但没完成步骤3-4outputs + review
处理:
- counter.release()v2.0:调用级生命周期)
- 续杯次数 +1
- 超过上限(3) → ❌ failed + escalate → counter.release()
- 未超限 → 🔄 用同一 session_id spawn(续杯),counter 不 release
- 超过上限(3) → ❌ failed + escalate
- 未超限 → 🔄 通过 spawn_full_agent 续杯(内部 can_acquire + acquire
- 续杯 message:提示 Agent 检查历史继续未完成工作
```
@@ -88,9 +120,10 @@ daemon:
原因:Gateway timeout 触发,Agent 连步骤1(写 working)都没来得及
处理:
- counter.release()v2.0:调用级生命周期)
- 续杯次数 +1
- 超过上限(3) → ❌ failed + escalate → counter.release()
- 未超限 → 🔄 用同一 session_id spawn(续杯),counter 不 release
- 超过上限(3) → ❌ failed + escalate
- 未超限 → 🔄 通过 spawn_full_agent 续杯
- 续杯 message:完整任务 prompt(和首次一样)
```
@@ -105,7 +138,7 @@ daemon:
处理:
- 记录 outcome = "agent_failed"
- counter.release()
- counter.release()(由 wrapped_on_complete 保证)
- 尊重 Agent 的判断,不续杯
- 如果 Agent 写了 detail,记录到事件
```
@@ -122,12 +155,12 @@ daemon:
fallback 用的是新 sessiongateway-fallback-* 前缀),不在原 session 里。
Agent 可能在 fallback 里完成了部分工作。
处理:
- 查任务实际 API 状态
- 如果已是 done/review → 同 A1
- 如果仍是 working/claimed → 同 A2/A3(续杯,用原 session_id,不用 fallback session
- 续杯 prompt 额外提示:"之前有 fallback 执行,请调 API 检查任务当前状态和已有产出,确认是否已完成"
- 记录 outcome = "fallback_timeout",附带 warning
处理v2.0
- counter.release()(由 wrapped_on_complete 保证)
- A5/A6 fallback 不应出现——出现说明 spawn 时 agent 被占用(L3 检查失效)
- 记录 ERROR 级日志,含 agent_id/session_id/task_id/transport/fallbackReason/counter_active
- 标 failed + escalate
- 不续杯
```
### A6exit=0 + transport=embedded + fallbackReason ≠ gateway_timeout
@@ -169,12 +202,10 @@ daemon:
原因:Gateway 进程挂了、网络断
处理:
- connect_retry_count +1(不计入 max_retries
- connect_retry_count ≥ 3 → ❌ failed + escalate → counter.release()
- 未超限 → 不改变任务状态(保持 claimed/working),不 release counter
- 只写 metadataoutcome=gateway_unreachable),让 ticker 下个 tick 自然重新调度
- ⚠️ 不在 spawner 里 sleep 等待(避免阻塞 monitor 逻辑)
处理v2.0
- counter.release()(进程退出 = release
- 不续杯,等 ticker 重新调度
- 记录 outcome = "gateway_unreachable"
```
### A9exit≠0 + stderr 含 rate_limit/500/503/API error
@@ -186,12 +217,11 @@ daemon:
原因:模型提供商限流、服务异常
处理:
- api_retry_count +1(不计入 max_retries
- api_retry_count ≥ 3 → ❌ failed + escalate → counter.release()
- 未超限 → 不改变任务状态,不 release counter
- 只写 metadataoutcome=api_error),让 ticker 下个 tick 自然重新调度
- ⚠️ 不在 spawner 里 sleep 等待
处理v2.0
- counter.release()(进程退出 = release
- 推回 pending(让 ticker 重新调度)
- counter.set_cooldown(agent_id, 120s)(冷却期,防止立即重试又 429)
- 不续杯
```
### A10exit≠0 + stderr 含 compaction-diag/context-overflow/timeout-compaction
@@ -203,10 +233,9 @@ daemon:
原因:compact 后模型返回错误(丢失上下文导致无法继续)
处理:
- retry_count +1(计入 max_retries
- 超过上限(3) → ❌ failed + escalate → counter.release()
- 未超限 → 🔄 用同一 session_id spawn(续杯),counter 不 release
处理v2.0
- counter.release()(进程退出 = release
- 不续杯,等 ticker 重新调度
- 记录 outcome = "compact_failed"
```
@@ -220,12 +249,10 @@ daemon:
原因:session lock 冲突(webchat/cron/其他 spawner 占用)
v2 用 --session-id uuid4 已基本避免,但保留兜底
处理:
- lock_retry_count +1(不计入 max_retries
- lock_retry_count ≥ 3 → ❌ failed + escalate → counter.release()
- 未超限 → 不改变任务状态,不 release counter
- 只写 metadataoutcome=lock_conflict),让 ticker 下个 tick 自然重新调度
- ⚠️ 不在 spawner 里 sleep 等待
处理v2.0
- counter.release()(进程退出 = release
- 不续杯,等 ticker 重新调度
- 记录 outcome = "lock_conflict"
```
### A12exit≠0 + stderr 无特殊关键字
@@ -237,14 +264,13 @@ daemon:
原因:Agent 自身逻辑错误、工具执行失败、或其他未知错误
处理:
- retry_count +1(计入 max_retries
- 超过上限(3) → ❌ failed + escalate → counter.release()
- 未超限 → 🔄 用同一 session_id spawn(续杯),counter 不 release
处理v2.0
- counter.release()(进程退出 = release
- 不续杯,等 ticker 重新调度
- 记录 outcome = "agent_error"
```
## 5. 情况 Bmonitor_timeout 到了进程还没退出
## 6. 情况 Bmonitor_timeout 到了进程还没退出
```
0s 600s 630s
@@ -264,11 +290,16 @@ daemon:
原因:Gateway 异常退出/崩溃,没有清理 lock 和 session 状态
子进程可能已经变成孤儿进程
处理:
- ❌ failed + escalate
- 不 kill(让用户决定)
- 记录 outcome = "session_stuck"
- escalate 消息中包含:PID、session key、诊断信息
处理v2.0:假死复活术)
1. 尝试复活:
a. 修改 sessions.json,把对应 session 的 status 从 running 改为 idle
b. release counter
c. ticker 下次 dispatch 时重新投递任务给 agent
2. 如果同一任务连续假死 ≥ 2 次:
- ❌ failed + escalate
- 记录 outcome = "session_stuck"
- escalate 消息中包含:PID、session key、诊断信息、假死次数
3. 不 kill(让用户决定)
```
### B2lock PID 存活 + sessions.json status=running + stderr 有 compact 关键字
@@ -331,7 +362,7 @@ daemon:
- 记录 outcome = "session_state_mismatch"
```
## 6. 续杯机制
## 7. 续杯机制
### 续杯触发条件
@@ -341,11 +372,13 @@ daemon:
| 任务类型 | Session 策略 | 说明 |
|---------|-------------|------|
| `_mail` 项目 | **主 Agent session**(不带 `--session-id` | Mail 投递到主 sessionGateway Queue 保证可靠排队,session lane 隔离 Mail 和 Task 互不干扰 |
| `_mail` 项目 | **主 Agent session**(不带 `--session-id` | Mail 投递到主 session |
| 普通任务 | 新 session`--session-id uuid4` | 未来可动态选择主/sub |
实现:`spawn_full_agent(use_main_session=True)` → 不传 `--session-id`dispatcher 根据 `project_id == "_mail"` 判断。
⚠️ **已知问题**Mail 用 main session 和 webchat/Control UI 共享同一 session。当 webchat 占用时,spawn 会等 session lock → timeout。通过 spawn 前 L3 session state 检查提前拦截。
### 续杯 message
```python
@@ -406,7 +439,7 @@ await self.spawner.spawn_full_agent(
)
```
## 7. 计数器设计
## 8. 计数器设计
| 计数器 | 用途 | 上限 | 超限处理 |
|--------|------|------|---------|
@@ -418,25 +451,28 @@ await self.spawner.spawn_full_agent(
存储在 `task_attempts.metadata` JSON 中。
### counter 生命周期
### counter 生命周期v2.0:调用级)
```
首次 acquiredispatcher.dispatch
spawn_full_agent 内部 acquire
├─ 续杯 spawn → counterrelease(保持占用)
├─ 继续等(B2/B3 → counter 不 release
├─ 暂时性失败回 tickerA8/A9/A11 → counter 不 release
├─ 进程退出 → wrapped_on_complete → counter.release()
├─ A1/A4 完成 → 结束
├─ A2/A3 timeout → _do_retry 手动 release → spawn_full_agent 重新 acquire
│ └─ A7-A12 → release → 等 ticker
最终完成/failed/escalate → counter.release()
monitor timeoutB→ counterrelease(进程还在跑)
│ └─ B1 假死 → 手动 release + 复活
└─ 进程崩溃/PM2 重启 → ticker _check_timeouts 检测 → 手动 release
```
counter 占用贯穿整个续杯链,只在以下情况 release
- 任务最终完成(A1/A4
- 超过重试上限 → failed + escalate
- 认证失败(A7
- 假死(B1
counter 生命周期 = 调用级:spawn 时 acquire,进程退出时 release
只有情况 B(进程不退出)counter 保持占用。
## 8. escalate 消息格式
wrapped_on_complete 保证 releasetry/finally),即使业务回调异常也不泄漏。
## 9. escalate 消息格式
```
⚠️ Agent {agent_id} 任务 {task_id} 执行异常
@@ -462,7 +498,7 @@ Session: {session_key}
请决定如何处理。
```
## 9. 改动范围
## 10. 改动范围
| 文件 | 改动 | 预估行数 |
|------|------|---------|
@@ -478,7 +514,7 @@ Session: {session_key}
总计约 280 行,3 个文件。
## 10. 测试计划
## 11. 测试计划
| 用例 | 模拟方式 | 验证 |
|------|---------|------|