feat(spawner): §24 compact detection via gateway log rotation events
This commit is contained in:
+92
-5
@@ -1297,6 +1297,88 @@ curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_
|
||||
logger.exception("Failed to revive %s", agent_id)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _get_recent_gateway_logs() -> list:
|
||||
"""获取当天和昨天的 gateway 日志路径。
|
||||
|
||||
日志路径通过 OPENCLAW_LOG_DIR 环境变量配置,默认 /tmp/openclaw。
|
||||
文件名格式:openclaw-{YYYY-MM-DD}.log
|
||||
"""
|
||||
from datetime import timedelta
|
||||
log_dir = os.environ.get("OPENCLAW_LOG_DIR", "/tmp/openclaw")
|
||||
now_local = datetime.now()
|
||||
today = now_local.strftime("%Y-%m-%d")
|
||||
yesterday = (now_local - 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
|
||||
|
||||
@staticmethod
|
||||
def _check_compact_in_progress_gateway(
|
||||
session_key: str, window_seconds: int = 120) -> bool:
|
||||
"""§24 v3 rotation-only: 检查 gateway 日志,判断指定 session 是否刚完成 compact。
|
||||
|
||||
检测逻辑:读日志尾部 2MB,按目标 sessionKey 过滤,
|
||||
找最后一个 rotation 事件,如果在窗口内 → compact 可能仍在 retry 循环中。
|
||||
"""
|
||||
from datetime import datetime as _dt, timezone as _tz, timedelta
|
||||
log_paths = AgentSpawner._get_recent_gateway_logs()
|
||||
if not log_paths:
|
||||
return False
|
||||
|
||||
now = _dt.now(_tz.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
|
||||
try:
|
||||
with open(log_path, "rb") as f:
|
||||
f.seek(0, 2)
|
||||
size = f.tell()
|
||||
f.seek(max(0, size - 2 * 1024 * 1024))
|
||||
tail = f.read().decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
for line in tail.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
continue
|
||||
|
||||
msg = obj.get("message", "")
|
||||
# 只看包含目标 sessionKey 的事件
|
||||
if session_key not in msg:
|
||||
continue
|
||||
|
||||
# rotation 事件
|
||||
if "[compaction] rotated active transcript" in msg:
|
||||
ts_str = obj.get("time", "")
|
||||
if ts_str:
|
||||
try:
|
||||
event_time = _dt.fromisoformat(
|
||||
ts_str.replace("Z", "+00:00"))
|
||||
# timezone-aware: normalize to UTC
|
||||
if event_time.tzinfo is None:
|
||||
event_time = event_time.replace(tzinfo=_tz.utc)
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def _check_recent_compaction_jsonl(
|
||||
session_file: str, window_seconds: int = 900) -> bool:
|
||||
@@ -1413,12 +1495,17 @@ curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# v2.8.1 Fix-1: compact 检测改用 session jsonl 末尾扫描
|
||||
# 只在 agent 非空闲时才扫描(减少不必要 I/O)
|
||||
# §24 v3: compact 检测优先用 gateway 日志 rotation 事件
|
||||
# 旧方法 _check_recent_compaction_jsonl 作为 fallback
|
||||
# 只在 agent 非空闲时才检查(减少不必要 I/O)
|
||||
if result["status"] not in (
|
||||
"done", "idle", "unknown", None) and sf:
|
||||
result["recent_compact"] = AgentSpawner._check_recent_compaction_jsonl(
|
||||
sf)
|
||||
"done", "idle", "unknown", None):
|
||||
session_key = f"agent:{agent_id}:main"
|
||||
result["recent_compact"] = AgentSpawner._check_compact_in_progress_gateway(
|
||||
session_key)
|
||||
if not result["recent_compact"] and sf:
|
||||
result["recent_compact"] = AgentSpawner._check_recent_compaction_jsonl(
|
||||
sf)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user