diff --git a/scripts/daemon.py b/scripts/daemon.py index 26177f56..df14262f 100755 --- a/scripts/daemon.py +++ b/scripts/daemon.py @@ -118,6 +118,7 @@ def log_reasoning_step(step_type, message, data=None): COMPACTION_MODEL = "glm-4-flash" COMPACTION_RATIO_THRESHOLD = 0.80 # Grok IntraCompactionTrigger 思路:token ratio > 80% 触发摘要 COMPACTION_MIN_TURNS = 10 # 至少 10 条 pattern 才压缩 +COMPACTION_MIN_INTERVAL = 300 # 距上次实际压缩至少 5 分钟(2026-09-06 防每 30s tick 空转刷日志) _compaction_last_run = 0 # 上次压缩时间戳 _compaction_count = 0 # 累计压缩次数 @@ -2399,7 +2400,9 @@ def deep_think(ctx, state, changes, journal, solutions_lib): if not result: return {"evaluation_previous_goal": "LLM调用失败", "memory": "上次调用失败", "next_goal": "重试"}, "" - log(f" 深度思考 ({tokens}t): {result[:200]}") + # 2026-09-06 修复:result 含换行时 log() 会把整段 LLM 输出折成多行续行 + # (每 2 分钟一次 deep_tick → 数十万续行),改为压缩成单行再写日志。 + log(f" 深度思考 ({tokens}t): {' '.join(result[:200].split())}") # Parse JSON output reflection_dict = {"evaluation_previous_goal": "", "memory": "", "next_goal": ""} @@ -2712,7 +2715,13 @@ def main_loop(): # ── Grok Build Compaction 系统:自动压缩检查 ─────────────────── state_for_compact = {"patterns": state.get("_patterns", []), "observations": state.get("_observations", [])} should_cp, cp_reason = _should_compact(ctx, state_for_compact) - if should_cp: + # 2026-09-06 修复:旧代码只要 _should_compact 返回 True(journal>150 常驻成立—— + # journal_entry→trim_journal 裁剪到 JOURNAL_MAX=200,永远 >150)就 log+尝试压缩, + # 而 patterns 为空时 _trigger_compaction 必然早退 → 每 30s light tick 刷一行 + # "🗜️ Compaction 触发",50 天刷了 10.3 万行(daemon.log 58MB 主源之一)。 + # 现在:patterns 不足 10 条不空跑;距上次实际压缩 <5 分钟不重复尝试。 + if should_cp and len(state_for_compact.get("patterns", [])) >= COMPACTION_MIN_TURNS \ + and time.time() - _compaction_last_run >= COMPACTION_MIN_INTERVAL: log(f" 🗜️ Compaction 触发: {cp_reason}") _trigger_compaction(ctx, state_for_compact) diff --git a/scripts/optimizer.py b/scripts/optimizer.py index df224ca8..9a9f4068 100755 --- a/scripts/optimizer.py +++ b/scripts/optimizer.py @@ -48,17 +48,45 @@ def collect_daemon_metrics(): metrics["learned_solutions"] = ctx.get("learned_count", 0) # 从 daemon.log 提取崩溃和错误 + # ⚠️ 2026-09-06 修复: + # 1) 只统计日志尾部 2MB(≈最近1-2天),避免把历史(如 07-25 NVML 6145 次崩溃)当"当前崩溃"; + # 2) 旧代码用子串 count("Error"/"error"/"失败") 会把 deep_think 的 LLM 输出文本 + # (如 soulful 行"沮丧情绪(失败)") 也数进去 → 报出 60295 个假错误。 + # 改为逐行分类:只统计 [DAEMON] 时间戳前缀行(daemon 自己的 log() 写的行), + # 跳过 LLM 输出续行;崩溃=行首"❌ 崩溃",错误=⚠️/[call_llm] 失败标记行。 log_file = D + "/daemon.log" if os.path.exists(log_file): - with open(log_file) as f: - content = f.read() - metrics["crashes"] = content.count("❌ 崩溃") - metrics["errors"] = content.count("Error") + content.count("error") + content.count("失败") - # 模型调用次数 - metrics["model_calls"] = content.count("深度思考") - # 从日志提取 token 数 - token_matches = re.findall(r'\((\d+)t\)', content) - metrics["model_tokens"] = sum(int(t) for t in token_matches) + with open(log_file, "rb") as f: + f.seek(0, 2) # 到末尾 + size = f.tell() + f.seek(max(0, size - 2 * 1024 * 1024)) # 尾部 2MB + content = f.read().decode("utf-8", errors="ignore") + line_re = re.compile(r"^\[DAEMON\] \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} (.*)$") + call_re = re.compile(r"^\[DAEMON\] \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\s+深度思考 \((\d+)t\)") + err_words = ("失败", "异常", "错误", "failed", "Failed", "超时", "timeout", "Timeout") + crashes = errors = model_calls = model_tokens = 0 + for line in content.splitlines(): + m = line_re.match(line) + if not m: + continue # LLM 输出续行等非 daemon 行,跳过 + cm = call_re.match(line) + if cm: + model_calls += 1 + model_tokens += int(cm.group(1)) + continue + msg = m.group(1).lstrip() + if msg.startswith("❌ 崩溃"): + crashes += 1 + elif (msg.startswith(("⚠️", "❌")) or msg.startswith("[call_llm]")) \ + and any(w in msg for w in err_words): + errors += 1 + elif "Traceback" in msg or re.search(r"\b(Error|ERROR|Exception)\b", msg): + errors += 1 + metrics["crashes"] = crashes + metrics["errors"] = errors + # 模型调用次数 + token 数(只从"深度思考 (Nt)"调用行取,N=该次调用 tokens) + metrics["model_calls"] = model_calls + metrics["model_tokens"] = model_tokens return metrics @@ -74,24 +102,31 @@ def collect_cron_metrics(): } # 从 cron/jobs.json 读取 + # ⚠️ 2026-09-06 修复:jobs.json 顶层是 dict {"jobs": [57个任务], "updated_at": ...}, + # 旧代码把 dict.values() 当任务列表 → total_jobs 只数到顶层键个数(2), + # 57 个真实任务全部漏计(报告显示 "2任务 0成功")。 jobs_file = HERMES + "/cron/jobs.json" if os.path.exists(jobs_file): with open(jobs_file) as f: try: - jobs = json.load(f) - # 处理不同格式 - if isinstance(jobs, dict): - jobs = [v for v in jobs.values()] - elif isinstance(jobs, list): - pass - + data = json.load(f) + if isinstance(data, dict): + jobs = data.get("jobs", []) + # 兜底: 兼容 {job_id: job} 形态 + if not isinstance(jobs, list) and jobs: + jobs = list(jobs.values()) + elif isinstance(data, list): + jobs = data + else: + jobs = [] + metrics["total_jobs"] = len(jobs) for j in jobs: if isinstance(j, dict): - status = j.get("last_status", "") + status = j.get("last_status") or "" if status == "ok": metrics["ok_jobs"] += 1 - elif status and status != "ok": + elif status in ("error", "failed"): metrics["failed_jobs"] += 1 if j.get("no_agent"): metrics["no_agent_jobs"] += 1