From 7918b12ff797b29b208be1fbe6b08d7d920dbd4f Mon Sep 17 00:00:00 2001 From: cfdaily Date: Thu, 11 Jun 2026 21:17:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(spawner):=20=C2=A724=20compact=20detection?= =?UTF-8?q?=20via=20gateway=20log=20rotation=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/design/07-spawner-acquire-first.md | 21 +- docs/design/13-toolchain-and-dev-workflow.md | 163 ++++++++++++++- docs/design/24-compact-detection-fix.md | 203 +++++++++++++++++++ src/daemon/spawner.py | 97 ++++++++- 4 files changed, 464 insertions(+), 20 deletions(-) create mode 100644 docs/design/24-compact-detection-fix.md diff --git a/docs/design/07-spawner-acquire-first.md b/docs/design/07-spawner-acquire-first.md index 8e39df0..a77a889 100644 --- a/docs/design/07-spawner-acquire-first.md +++ b/docs/design/07-spawner-acquire-first.md @@ -233,20 +233,27 @@ def _revive_session(agent_id: str) -> bool: pass ``` -### 4.5 O5: compact 扫描条件收紧 +### 4.5 O5: compact 检测(§24 rotation-only v3) -当前 compact 扫描在 status 非 idle/done/unknown/None 时都触发,范围过宽。 +§24 设计文档:`docs/design/24-compact-detection-fix.md` -**改后**:只在 status 为 running 或 compacting 相关时扫描: +**检测方法**:读 gateway 日志尾部 2MB,按 sessionKey 过滤 `[compaction] rotated active transcript` 事件。 +如果最近的 rotation 事件在 120s 窗口内 → 视为 compact 循环进行中(可能还在 post-compact retry)。 + +旧方法 `_check_recent_compaction_jsonl`(扫描 session jsonl 的 `type=compaction` 事件)保留作为 fallback。 ```python -# 只在这些状态下检查 compact -if result["status"] in ("running",) and sf: - result["recent_compact"] = AgentSpawner._check_recent_compaction_jsonl(sf) +# §24 v3: compact 检测优先用 gateway 日志 rotation 事件 +if result["status"] not in ("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) ``` 注:Gateway 的 sessions.json status 实际值主要是 `idle/running/timeout/failed`。 -`running` 时检查 compact 有意义(agent turn 执行中可能触发 compact)。 +非空闲状态(`running`/`timeout`/`failed`)时检查 compact 有意义。 其他状态不需要检查。 ## 五、改动范围 diff --git a/docs/design/13-toolchain-and-dev-workflow.md b/docs/design/13-toolchain-and-dev-workflow.md index 1040797..4d44082 100644 --- a/docs/design/13-toolchain-and-dev-workflow.md +++ b/docs/design/13-toolchain-and-dev-workflow.md @@ -33,26 +33,173 @@ | 项 | 配置 | |----|------| | 地址 | `http://192.168.2.154:3000` | -| 版本 | v1.23.4 | -| 认证 | HTTP + token(待配置) | -| 权限 | cfdaily 用户;姜维持有 admin 权限(启用 Actions、分支保护等) | +| 版本 | v1.26.2(2026-06-11 从 v1.23.4 升级) | +| 认证 | HTTP + token;admin 账号(姜维持有) | +| 权限 | 姜维持有 admin 权限(启用 Actions、分支保护、org webhook 等) | +| 数据库 | SQLite3 | +| 部署方式 | Docker(NAS 群晖),数据卷 `/volume2/@docker/volumes/gitea-data/_data` | ### 2.2 CI/CD:Gitea Actions | 项 | 配置 | |----|------| -| Runner | Mac mini 裸机,act-runner(Go 二进制) | +| Runner | Mac mini 裸机,gitea-runner v1.0.8(通过 PM2 管理 `sanguo-act-runner`) | | 配置文件 | `.gitea/workflows/*.yml`,每个项目自管 | -| 语法 | 兼容 GitHub Actions(v1.23.4 已验证支持) | -| 触发 | push / PR / tag | +| 语法 | 兼容 GitHub Actions(v1.26.2 已验证支持 concurrency groups) | +| 触发 | push / PR / tag / workflow_dispatch | +| v1.26 新增 | concurrency groups、re-run failed jobs、可配置 GITEA_TOKEN 权限 | +| 仍不支持 | `failure()`、`continue-on-error`、`timeout-minutes` | -### 2.3 部署目标 +### 2.4 Gitea 基础设施 Setup 记录(2026-06-11 姜维) + +> 以下为 Gitea 从 v1.23.4 升级到 v1.26.2 的完整操作记录,作为未来参考。 + +#### 2.4.1 升级 v1.23.4 → v1.26.2 + +**升级原因**:v1.23.4 不支持 concurrency groups,导致双倍触发问题无根因解法。 + +**升级步骤**: +1. 备份:`docker exec sanguo_gitea gitea dump -c /data/gitea/conf/app.ini -f /data/gitea/gitea-backup-pre-v126.zip`(765MB) +2. 拉取镜像:Mac 上 skopeo 下载 → python docker SDK 远程 load(群晖 Docker Hub 太慢) +3. 停止旧容器 + rename 保留回滚 +4. 启动新容器(数据库自动迁移 Migration[312]→[326],含 concurrency #323) +5. 验证:API + Web UI + 仓库数据 + 用户数据 + +**踩坑:群晖内核 3.10 + git 2.52 不兼容**: +- 根因:git 2.52 使用 `getrandom(2)` syscall,群晖内核 3.10.108 不支持(3.17 才加入) +- 症状:`git push` 报 `unable to create temporary file: Function not implemented` +- 修复:entrypoint 脚本在容器启动时自动从本地缓存降级 git 到 2.45.4 +- 持久化:`/data/entrypoint-wrapper.sh` + `/data/git-2.45.4-r0.apk` 在数据卷里,容器重建不丢失 +- 群晖内核**无法通过 DSM 升级**,内核版本跟硬件型号绑定 + +**完整重建命令**: +```bash +docker -H tcp://192.168.2.154:2375 run -d \ + --name sanguo_gitea \ + --restart=always \ + -p 3000:3000 \ + -p 2221:22 \ + -v /volume2/@docker/volumes/gitea-data/_data:/data \ + -e GITEA__database__DB_TYPE=sqlite3 \ + -e GITEA__database__PATH=/data/gitea/gitea.db \ + -e GITEA__server__ROOT_URL=http://192.168.2.154:3000/ \ + --entrypoint /bin/sh \ + gitea/gitea:1.26.2 \ + -c '/data/entrypoint-wrapper.sh' +``` + +#### 2.4.2 act_runner 升级 v0.2.11 → v1.0.8 + +**升级原因**:v0.2.11 的 multi-step job 执行有 bug,Setup Python 和 Lint step 被跳过。 + +**升级步骤**: +1. 下载 `gitea-runner-1.0.8-darwin-arm64`(从 gitea.com releases) +2. `codesign --force --sign -` 重签(macOS Gatekeeper 会 SIGKILL 未签名的二进制) +3. 替换 `/Users/chufeng/bin/act_runner` +4. PM2 restart `sanguo-act-runner` + +**注意**:act_runner 通过 **PM2** 管理(`sanguo-act-runner`),不是 launchd。launchd plist 仅为备份。 + +**PM2 常用命令**: +```bash +pm2 restart sanguo-act-runner # 重启 +pm2 logs sanguo-act-runner # 查看日志 +pm2 show sanguo-act-runner # 详情 +``` + +#### 2.4.3 CI Workflow 配置 + +**三个 workflow 文件**: + +| 文件 | 触发 | concurrency | 说明 | +|------|------|-------------|------| +| `ci.yml` | `pull_request` | `group: ci-${{ gitea.ref }}, cancel-in-progress: true` | 同一 PR 新 push 自动取消旧 run | +| `deploy.yml` | `push to main` | `group: deploy-${{ gitea.ref }}, cancel-in-progress: false` | 部署排队不取消 | +| `e2e.yml` | `workflow_dispatch` | `group: e2e-${{ gitea.ref }}, cancel-in-progress: true` | 手动触发 | + +**Branch Protection(main 分支)**: +- 禁止直接 push +- status check:`CI / lint (pull_request)` 必须通过 +- 至少 1 人 Review + +**⚠️ 踩坑**:v1.26 上报的 commit status context 格式变了: +- 旧格式:`lint` +- 新格式:`CI / lint (pull_request)` +- branch protection 必须用新格式匹配,否则 merge 报 "Not all required status checks successful" + +#### 2.4.4 Org Webhook 配置 + +- **对象**:Gitea 组织 `sanguo` webhook id=28 +- **URL**:`http://192.168.2.153:8083/webhook/gitea` +- **事件**:16 个(push/issues/PR/PR review 等) + +**⚠️ 踩坑**:Gitea v1.26 的 PATCH hooks API,只传 `{"active": true}` 会把 events 重置为 `["push"]`。**必须每次 PATCH 都带上完整的 events 列表。** + +**临时措施(已恢复)**:2026-06-10 曾临时关闭 webhook(CI 错误大爆炸期间),2026-06-11 已恢复。 + +#### 2.4.5 凭据管理 + +| 凭据 | 用途 | 持有者 | +|------|------|--------| +| Gitea admin:cf7561523 | 仓库管理、branch protection、org webhook | 姜维 | +| Gitea PAT (jiangwei-infra) | API 操作、git clone/push | 姜维 | +| Gitea PAT (cfdaily) | CI workflow 中的 git 操作 | CI secrets | + +#### 2.4.6 备份与回滚 + +| 项目 | 路径 | 说明 | +|------|------|------| +| Gitea 数据库备份 | `/data/gitea/gitea-backup-pre-v126.zip` | 升级前 dump | +| 旧容器 | 已清理 | 升级验证通过后 `docker rm` | +| 变更记录 | `~/.openclaw/workspace-jiangwei/changes/gitea-emergency-2026-06-10.md` | 完整操作日志 | | 环境 | 位置 | 说明 | |------|------|------| | Mac mini 本机 | `~/.sanguo_projects//` | 主力开发和运行环境 | | NAS Docker | `192.168.2.154` | 部分服务(Gitea、回测等) | +#### 2.4.7 Gitea 迁移验证记录(2026-06-11 司马懿) + +> 验证 Gitea 从 gitee 迁移完成后的状态。所有验证在 2026-06-11 完成。 + +**仓库迁移状态**: + +| 项目 | Gitea 仓库 | 开发目录 | 远程地址 | gitee 残留 | +|------|-----------|---------|---------|----------| +| sanguo_moziplus_v2 | `sanguo/sanguo_moziplus_v2` | `~/.openclaw/sanguo_projects/sanguo_moziplus_v2/` | `http://192.168.2.154:3000/sanguo/sanguo_moziplus_v2.git` | ✅ 无 | +| sanguo_quant_live | `sanguo/sanguo_quant_live` | `~/.openclaw/sanguo_projects/sanguo_quant_live/` | `http://192.168.2.154:3000/sanguo/sanguo_quant_live.git` | ✅ 无 | +| sanguo_vnpy | `sanguo/sanguo_vnpy` | `~/.openclaw/sanguo_projects/sanguo_vnpy/` | `http://192.168.2.154:3000/sanguo/sanguo_vnpy.git` | ✅ 无 | + +**验证方法**:在 3 个开发目录分别执行 `git remote -v`,确认 origin 指向 gitea 且无 gitee remote。 + +**CI 管道验证**: + +| 验证项 | 结果 | 备注 | +|--------|------|------| +| PR #33 Lint 修复 CI 通过 | ✅ | flake8 全通过 | +| CD pipeline (deploy.yml) 合并 | ✅ | 含 CI + deploy + notify-deploy-failure 三个 job | +| Branch protection 生效 | ✅ | main 分支需 CI 通过 + 1 人 Review 才能合并 | +| Gitea squash merge 兼容 | ✅ | `merge_commit_sha` 在 squash merge 下仍等于 gitea.sha | + +**工具链事件中枢验证**: + +| Webhook → Mail 流 | 验证结果 | +|-------------------|--------| +| PR opened → Review 请求 Mail | ✅ 司马懿收到 PR #30-#35 的 Review 请求 | +| PR review → 结果 Mail | ✅ 张飞/庞统收到 Review 结果通知 | +| Issue assigned → 指派 Mail | ✅ (E2E 验证通过) | +| CI 失败评论 → 通知 Mail | ✅ (E2E 验证通过) | +| PR synchronize → reviewer 重审 Mail | ✅ 新增(§23) | +| Review COMMENTED → PR 作者通知 | ✅ 新增(§23) | + +**Agent Gitea 凭据**(各 Agent 自行持有 PAT): + +| Agent | Gitea 用户名 | PAT 用途 | +|-------|-------------|--------:| +| simayi-challenger | simayi-challenger | PR Review 提交 | +| pangtong-fujunshi | pangtong-fujunshi | PR 创建/合并、代码 push | +| jiangwei-infra | jiangwei-infra | 基础设施配置(admin 级操作) | + --- ## §3. 分支策略 @@ -157,7 +304,7 @@ Open → In Progress → Review → Closed 每个项目在 `.gitea/workflows/ci.yml` 自定义具体步骤,但遵循统一骨架。 -> **注**:Gitea Actions v1.23.4 不支持 `paths` 过滤触发条件。通过路径判断放在 job 级别的 `if` 条件中,使用确定支持的语法。(M4 修订) +> **注**:Gitea Actions v1.26.2 不支持 `paths` 过滤触发条件。通过路径判断放在 job 级别的 `if` 条件中,使用确定支持的语法。(M4 修订) ```yaml name: CI diff --git a/docs/design/24-compact-detection-fix.md b/docs/design/24-compact-detection-fix.md new file mode 100644 index 0000000..799fd4e --- /dev/null +++ b/docs/design/24-compact-detection-fix.md @@ -0,0 +1,203 @@ +# §24 — Compact 检测方案修正 + +> 状态:v3(rotation-only),待实施 +> 作者:庞统 +> 日期:2026-06-11 +> 框架:基于 §07 Spawner Acquire-First +> 评审:仲达 3 轮评审(v1 trajectory → v2 gateway precheck → v3 rotation-only) + +## 1. 问题 + +### 1.1 现象 + +2026-06-11 14:02,pangtong main session 正在做 compaction(13: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. 从后往前找最后一条相关事件: + a. 如果是 rotation 且 < N 秒(建议 120s)→ compact=True + (刚完成一轮 compact,可能还在 post-compact retry 循环中) + b. 如果是 model.completed 或其他正常事件 → compact=False + c. 超出时间窗口 → compact=False +``` + +**为什么 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 间隔通常 <60s,120s 有足够余量 + +误判代价低(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_event_type = None + last_event_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 event_time > last_event_time if last_event_time else True: + last_event_time = event_time + last_event_type = "rotation" + except (ValueError, TypeError): + continue + + if last_event_type == "rotation" and last_event_time: + return last_event_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. 边界情况 + +| 边界情况 | 处理 | +|---------|------| +| 日志文件不存在 | 返回 False(fallback 到旧方法) | +| 跨天 compact | 同时检查昨天日志尾部 | +| compact 失败(无 rotation) | rotation 事件不会出现 → 检测不到 → 回退到旧方法 | +| 误判(compact 完成后正常工作中) | 时间窗口 120s 内正常 turn 的 model.completed 事件会覆盖 rotation | + +## 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 Optimization(compact_hanging 处理) + +## 8. 评审记录 + +- **v1**:trajectory jsonl 间接推断 → 仲达指出 trajectoryPath 不可用、需多文件等 3 个问题 +- **v2**:gateway 日志 precheck 开始标志 → 仲达指出开始标志覆盖率仅 30%,建议 rotation-only +- **v3**:rotation-only(当前版本)→ 仲达已确认方向,待代码实现后再审 diff --git a/src/daemon/spawner.py b/src/daemon/spawner.py index fb0c315..702cebf 100644 --- a/src/daemon/spawner.py +++ b/src/daemon/spawner.py @@ -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