fix(spawner): address PR#36 review feedback (M1+M2+S1+S2)
This commit is contained in:
@@ -244,7 +244,7 @@ def _revive_session(agent_id: str) -> bool:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# §24 v3: compact 检测优先用 gateway 日志 rotation 事件
|
# §24 v3: compact 检测优先用 gateway 日志 rotation 事件
|
||||||
if result["status"] not in ("done", "idle", "unknown", None):
|
if result["status"] not in ("idle", "unknown", None):
|
||||||
session_key = f"agent:{agent_id}:main"
|
session_key = f"agent:{agent_id}:main"
|
||||||
result["recent_compact"] = AgentSpawner._check_compact_in_progress_gateway(
|
result["recent_compact"] = AgentSpawner._check_compact_in_progress_gateway(
|
||||||
session_key)
|
session_key)
|
||||||
|
|||||||
@@ -45,11 +45,15 @@ Gateway 日志中 `[compaction] rotated active transcript after compaction (sess
|
|||||||
```
|
```
|
||||||
1. 读 gateway 日志(当天 + 昨天尾部)
|
1. 读 gateway 日志(当天 + 昨天尾部)
|
||||||
2. 按目标 sessionKey 过滤 compact 相关事件
|
2. 按目标 sessionKey 过滤 compact 相关事件
|
||||||
3. 从后往前找最后一条相关事件:
|
3. 从后往前找最后一条 rotation 事件:
|
||||||
a. 如果是 rotation 且 < N 秒(建议 120s)→ compact=True
|
a. 如果 rotation 事件在窗口内(< 120s)→ compact=True
|
||||||
(刚完成一轮 compact,可能还在 post-compact retry 循环中)
|
(刚完成一轮 compact,可能还在 post-compact retry 循环中)
|
||||||
b. 如果是 model.completed 或其他正常事件 → compact=False
|
b. 无 rotation 事件或超出时间窗口 → compact=False
|
||||||
c. 超出时间窗口 → compact=False
|
|
||||||
|
**注意:此方案仅检查 rotation 事件,不检查 model.completed 等其他事件。**
|
||||||
|
这是有意为之的保守策略:不检查正常 turn 事件意味着 compact 完成后的
|
||||||
|
120s 内都可能被误判为 compact 进行中,但误判代价低(仅 skip 一轮 ticker),
|
||||||
|
宁可多拦也不漏放。
|
||||||
```
|
```
|
||||||
|
|
||||||
**为什么 rotation + 时间窗口就够了?**
|
**为什么 rotation + 时间窗口就够了?**
|
||||||
@@ -97,8 +101,7 @@ def _check_compact_in_progress_gateway(self, session_key: str, window_seconds: i
|
|||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
window_start = now - timedelta(seconds=window_seconds)
|
window_start = now - timedelta(seconds=window_seconds)
|
||||||
|
|
||||||
last_event_type = None
|
last_rotation_time = None
|
||||||
last_event_time = None
|
|
||||||
|
|
||||||
for log_path in log_paths:
|
for log_path in log_paths:
|
||||||
if not os.path.exists(log_path):
|
if not os.path.exists(log_path):
|
||||||
@@ -127,14 +130,13 @@ def _check_compact_in_progress_gateway(self, session_key: str, window_seconds: i
|
|||||||
if "[compaction] rotated active transcript" in msg:
|
if "[compaction] rotated active transcript" in msg:
|
||||||
try:
|
try:
|
||||||
event_time = datetime.fromisoformat(ts_str)
|
event_time = datetime.fromisoformat(ts_str)
|
||||||
if event_time > last_event_time if last_event_time else True:
|
if last_rotation_time is None or event_time > last_rotation_time:
|
||||||
last_event_time = event_time
|
last_rotation_time = event_time
|
||||||
last_event_type = "rotation"
|
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if last_event_type == "rotation" and last_event_time:
|
if last_rotation_time is not None:
|
||||||
return last_event_time >= window_start
|
return last_rotation_time >= window_start
|
||||||
|
|
||||||
return False
|
return False
|
||||||
```
|
```
|
||||||
@@ -174,7 +176,7 @@ if compact:
|
|||||||
| 日志文件不存在 | 返回 False(fallback 到旧方法) |
|
| 日志文件不存在 | 返回 False(fallback 到旧方法) |
|
||||||
| 跨天 compact | 同时检查昨天日志尾部 |
|
| 跨天 compact | 同时检查昨天日志尾部 |
|
||||||
| compact 失败(无 rotation) | rotation 事件不会出现 → 检测不到 → 回退到旧方法 |
|
| compact 失败(无 rotation) | rotation 事件不会出现 → 检测不到 → 回退到旧方法 |
|
||||||
| 误判(compact 完成后正常工作中) | 时间窗口 120s 内正常 turn 的 model.completed 事件会覆盖 rotation |
|
| 误判(compact 完成后正常工作中) | 时间窗口 120s 内可能被误判,但代价低(skip 一轮 ticker)。不检查正常 turn 事件,是保守策略 |
|
||||||
|
|
||||||
## 6. 测试验证
|
## 6. 测试验证
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
"""单元测试:§24 v3 rotation-only compact 检测
|
||||||
|
|
||||||
|
测试 _get_recent_gateway_logs 和 _check_compact_in_progress_gateway。
|
||||||
|
用 tmp_path 构造 mock gateway 日志文件。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.daemon.spawner import AgentSpawner
|
||||||
|
|
||||||
|
|
||||||
|
# ── helpers ──
|
||||||
|
|
||||||
|
_SESSION_KEY = "agent:pangtong-fujunshi:main"
|
||||||
|
_TODAY_STR = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def _make_rotation_event(session_key: str, ts: datetime) -> dict:
|
||||||
|
"""构造一条 rotation 日志事件"""
|
||||||
|
return {
|
||||||
|
"time": ts.isoformat(),
|
||||||
|
"message": f"[compaction] rotated active transcript after compaction (sessionKey={session_key})",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _make_other_event(session_key: str, ts: datetime, msg: str = "something else") -> dict:
|
||||||
|
"""构造一条普通日志事件"""
|
||||||
|
return {
|
||||||
|
"time": ts.isoformat(),
|
||||||
|
"message": f"{msg} (sessionKey={session_key})",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _write_log(tmp_path: Path, date_str: str, lines: list[dict]):
|
||||||
|
"""写 mock 日志文件"""
|
||||||
|
log_file = tmp_path / f"openclaw-{date_str}.log"
|
||||||
|
with open(log_file, "w") as f:
|
||||||
|
for obj in lines:
|
||||||
|
f.write(json.dumps(obj, ensure_ascii=False) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _set_log_dir(tmp_path, monkeypatch):
|
||||||
|
"""每个测试自动设置 OPENCLAW_LOG_DIR 到 tmp_path"""
|
||||||
|
monkeypatch.setenv("OPENCLAW_LOG_DIR", str(tmp_path))
|
||||||
|
|
||||||
|
|
||||||
|
# ── 测试用例 ──
|
||||||
|
|
||||||
|
|
||||||
|
class TestCheckCompactInProgress:
|
||||||
|
"""§24 v3: _check_compact_in_progress_gateway 单元测试"""
|
||||||
|
|
||||||
|
def test_rotation_within_window_returns_true(self, tmp_path):
|
||||||
|
"""TC1: rotation 事件在窗口内 → True"""
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
recent = now - timedelta(seconds=30)
|
||||||
|
_write_log(tmp_path, _TODAY_STR, [_make_rotation_event(_SESSION_KEY, recent)])
|
||||||
|
assert AgentSpawner._check_compact_in_progress_gateway(_SESSION_KEY) is True
|
||||||
|
|
||||||
|
def test_rotation_outside_window_returns_false(self, tmp_path):
|
||||||
|
"""TC2: rotation 事件超出窗口 → False"""
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
old = now - timedelta(seconds=200)
|
||||||
|
_write_log(tmp_path, _TODAY_STR, [_make_rotation_event(_SESSION_KEY, old)])
|
||||||
|
assert AgentSpawner._check_compact_in_progress_gateway(_SESSION_KEY) is False
|
||||||
|
|
||||||
|
def test_no_rotation_event_returns_false(self, tmp_path):
|
||||||
|
"""TC3: 无 rotation 事件 → False"""
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
_write_log(tmp_path, _TODAY_STR, [
|
||||||
|
_make_other_event(_SESSION_KEY, now, "model.completed"),
|
||||||
|
])
|
||||||
|
assert AgentSpawner._check_compact_in_progress_gateway(_SESSION_KEY) is False
|
||||||
|
|
||||||
|
def test_log_file_not_exists_returns_false(self, tmp_path):
|
||||||
|
"""TC4: 日志文件不存在 → False"""
|
||||||
|
# tmp_path 为空目录,无日志文件
|
||||||
|
assert AgentSpawner._check_compact_in_progress_gateway(_SESSION_KEY) is False
|
||||||
|
|
||||||
|
def test_session_key_mismatch_returns_false(self, tmp_path):
|
||||||
|
"""TC5: sessionKey 不匹配 → False"""
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
recent = now - timedelta(seconds=10)
|
||||||
|
other_key = "agent:simayi-challenger:main"
|
||||||
|
_write_log(tmp_path, _TODAY_STR, [_make_rotation_event(other_key, recent)])
|
||||||
|
assert AgentSpawner._check_compact_in_progress_gateway(_SESSION_KEY) is False
|
||||||
Reference in New Issue
Block a user