Files
sanguo_moziplus_v2/docs/design/24-compact-detection-fix.md
T
cfdaily bcb8ced17a
CI / lint (pull_request) Successful in 8s
CI / test (pull_request) Successful in 9s
CI / notify-on-failure (pull_request) Successful in 0s
fix(spawner): address PR#36 review feedback (M1+M2+S1+S2)
2026-06-11 21:40:09 +08:00

206 lines
7.8 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.
# §24 — Compact 检测方案修正
> 状态:v3rotation-only),待实施
> 作者:庞统
> 日期:2026-06-11
> 框架:基于 §07 Spawner Acquire-First
> 评审:仲达 3 轮评审(v1 trajectory → v2 gateway precheck → v3 rotation-only
## 1. 问题
### 1.1 现象
2026-06-11 14:02pangtong main session 正在做 compaction13:59:26 开始,14:06:00 结束,耗时 ~6.5 分钟),但 spawner Phase 2 检查时 `compact=False`,仍然 spawn 了新进程处理 Mail,导致两个 agent turn 撞车。
### 1.2 根因
当前 compact 检测方法 `_check_recent_compaction_jsonl` 扫描 session jsonl,查找 `type == "compaction"` 事件。这是 compact **完成后**才写入的摘要记录,compact **进行中**时不存在 → 漏检。
同时 Gateway 触发 compact 时先把 session 标为 `done`,所以 `status=running + lock_pid_alive` 检查也无效。14:02:11 实际状态:`status=done lock_pid_alive=False compact=False`——三个检查全部漏过。
## 2. 方案:Rotation-Only 检测(v3
### 2.1 核心洞察(仲达 v2 评审)
v2 方案依赖 `[context-overflow-precheck]` route=compact 作为开始标志。但实测数据:
| Agent | Rotation 事件 | 有 Precheck | 无 Precheck |
|-------|:---:|:---:|:---:|
| pangtong | 7 | 3 | 4 |
| simayi | 3 | 0 | 3 |
**10 次 compact 只有 3 次有 precheck,覆盖率 30%。** 原因:post-compact retry 触发的后续 compact 不经过 precheck 日志路径。
**结论**:开始标志不可靠。反转检测逻辑——只用可靠的 rotation 事件作为信号。
### 2.2 Rotation 事件
Gateway 日志中 `[compaction] rotated active transcript after compaction (sessionKey=...)` 事件:
- **100% 覆盖率**:全天 10 次 compact 全部有 rotation 事件
- **含 sessionKey**:可以精确匹配目标 session
- **JSON 格式**:易解析
### 2.3 检测逻辑
```
1. 读 gateway 日志(当天 + 昨天尾部)
2. 按目标 sessionKey 过滤 compact 相关事件
3. 从后往前找最后一条 rotation 事件:
a. 如果 rotation 事件在窗口内(< 120s)→ compact=True
(刚完成一轮 compact,可能还在 post-compact retry 循环中)
b. 无 rotation 事件或超出时间窗口 → compact=False
**注意:此方案仅检查 rotation 事件,不检查 model.completed 等其他事件。**
这是有意为之的保守策略:不检查正常 turn 事件意味着 compact 完成后的
120s 内都可能被误判为 compact 进行中,但误判代价低(仅 skip 一轮 ticker),
宁可多拦也不漏放。
```
**为什么 rotation + 时间窗口就够了?**
- compact 后 Gateway 会 retry prompt
- 如果 retry 又触发 overflow → 又一轮 compact → 又一个 rotation 事件
- 如果 retry 成功 → 正常 turn → 新的 session.started / model.completed 事件
- 所以「最近一个事件是 rotation 且时间很近」= compact 循环还在进行
### 2.4 时间窗口选择
compact 通常耗时 1-10 分钟。post-compact retry 如果又触发 compact,间隔通常 <60 秒。
- **窗口太短(如 30s)**:可能漏掉 compact 结束后正在 retry 但还没触发下一轮的场景
- **窗口太长(如 900s)**:compact 完成后正常工作很久了还误判
- **推荐 120s**compact 循环中两次 rotation 间隔通常 <60s120s 有足够余量
误判代价低(skip 一轮 ticker),所以宁可多拦也不漏放。
## 3. 改动范围
| 文件 | 改动 | 行数估计 |
|------|------|---------|
| `spawner.py` | 新增 `_check_compact_in_progress_gateway()` | ~40 行 |
| `spawner.py` | `_check_session_state()` 调用新方法,替换旧方法 | ~5 行 |
| `spawner.py` | 日志路径配置化 | ~5 行 |
| `docs/design/07-spawner-acquire-first.md` | §4.5 O5 更新 | ~10 行 |
| `docs/design/24-compact-detection-fix.md` | 本文档 | 已有 |
**总计 ~60 行代码改动。**
## 4. 实现细节
### 4.1 核心方法
```python
def _check_compact_in_progress_gateway(self, session_key: str, window_seconds: int = 120) -> bool:
"""检查 gateway 日志,判断指定 session 是否刚完成 compact(可能在 retry 循环中)。
检测逻辑:如果目标 session 最近一个事件是 rotation 且在窗口内,视为 compact 进行中。
"""
log_paths = self._get_recent_gateway_logs()
if not log_paths:
return False
now = datetime.now(timezone.utc)
window_start = now - timedelta(seconds=window_seconds)
last_rotation_time = None
for log_path in log_paths:
if not os.path.exists(log_path):
continue
with open(log_path, 'rb') as f:
# 读尾部 2MB
f.seek(0, 2)
size = f.tell()
f.seek(max(0, size - 2 * 1024 * 1024))
for raw_line in f:
try:
obj = json.loads(raw_line)
except (json.JSONDecodeError, ValueError):
continue
msg = obj.get("message", "")
ts_str = obj.get("time", "")
# 只看包含目标 sessionKey 的事件
if session_key not in msg:
continue
# rotation 事件
if "[compaction] rotated active transcript" in msg:
try:
event_time = datetime.fromisoformat(ts_str)
if last_rotation_time is None or event_time > last_rotation_time:
last_rotation_time = event_time
except (ValueError, TypeError):
continue
if last_rotation_time is not None:
return last_rotation_time >= window_start
return False
```
### 4.2 日志路径
```python
def _get_recent_gateway_logs(self) -> list:
"""获取当天和昨天的 gateway 日志路径"""
log_dir = os.environ.get("OPENCLAW_LOG_DIR", "/tmp/openclaw")
today = datetime.now().strftime("%Y-%m-%d")
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
paths = []
for d in [today, yesterday]:
p = os.path.join(log_dir, f"openclaw-{d}.log")
if os.path.exists(p):
paths.append(p)
return paths
```
### 4.3 Phase 2 集成
```python
# 在 _check_session_state 中,不依赖 status,直接检查
compact = self._check_compact_in_progress_gateway(session_key)
if not compact:
compact = self._check_recent_compaction_jsonl(...) # fallback
if compact:
blockers.append(("session_compacting", None))
```
## 5. 边界情况
| 边界情况 | 处理 |
|---------|------|
| 日志文件不存在 | 返回 Falsefallback 到旧方法) |
| 跨天 compact | 同时检查昨天日志尾部 |
| compact 失败(无 rotation | rotation 事件不会出现 → 检测不到 → 回退到旧方法 |
| 误判(compact 完成后正常工作中) | 时间窗口 120s 内可能被误判,但代价低(skip 一轮 ticker)。不检查正常 turn 事件,是保守策略 |
## 6. 测试验证
### 6.1 单元测试
- `_check_compact_in_progress_gateway`
- rotation 事件在窗口内 → True
- rotation 事件超出窗口 → False
- 无 rotation 事件 → False
- 日志不存在 → False
- sessionKey 不匹配 → False
### 6.2 集成验证
- `pytest -m "not e2e"` 全量测试
## 7. 关联设计
- §07 Spawner Acquire-First(§4.5 O5 compact 扫描条件收紧)
- §08 Classify Outcome Optimizationcompact_hanging 处理)
## 8. 评审记录
- **v1**trajectory jsonl 间接推断 → 仲达指出 trajectoryPath 不可用、需多文件等 3 个问题
- **v2**gateway 日志 precheck 开始标志 → 仲达指出开始标志覆盖率仅 30%,建议 rotation-only
- **v3**rotation-only(当前版本)→ 仲达已确认方向,待代码实现后再审