diff --git a/docs/design/24-compact-detection-fix.md b/docs/design/24-compact-detection-fix.md index 86ab53e..431febf 100644 --- a/docs/design/24-compact-detection-fix.md +++ b/docs/design/24-compact-detection-fix.md @@ -1,12 +1,54 @@ # §24 — Compact 检测方案修正 -> 状态:v4(trajectory prompt.submitted),待实施 +> 状态:**v5 已实现**(gateway log + jsonl 配对) > 作者:庞统 -> 日期:2026-06-11 +> 日期:2026-06-11(v4),2026-06-13(v5) > 框架:基于 §07 Spawner Acquire-First -> 评审:仲达 4 轮评审(v1 trajectory → v2 gateway precheck → v3 rotation-only → v4 prompt.submitted) +> 评审:仲达 4+2 轮评审 > 备选方案:B(内存 flag + sessions.json status),见 §2B +--- + +## 0. v5 方案(已实现) + +### 0.1 方案概述 + +**gateway log 开始标记(precheck `route=compact_then_truncate`)+ jsonl 结束标记(`type: "compaction"` entry)配对**。 + +- **开始标记**:扫描 gateway 日志,找含目标 agent sessionKey 且 `route=compact_then_truncate` 的 precheck 日志行,提取时间戳。 +- **结束标记**:扫描 session jsonl,找开始时间之后的 `type: "compaction"` entry。 +- **判定逻辑**:有开始无结束 → compact 进行中 → skip ticker;有开始有结束 → compact 已完成 → 不 skip。 +- **超时兜底**:开始标记超过 15 分钟仍未结束 → 自动忽略(防止死锁)。 + +### 0.2 三种 Compact 触发路径分析 + +Gateway 的 compact 有多种触发路径,日志表现不同: + +| 触发路径 | 有开始标记? | 有 sessionKey? | 有 compaction 结束标记? | 检测策略 | +|---------|------------|---------------|----------------------|--------| +| **overflow** | 有(`attempting auto-compaction`) | ❌ 不含 | 有 | 依赖 precheck 覆盖 | +| **timeout** | 有(`[timeout-compaction]` + `attempting`) | ❌ 推测不含 | 有 | 依赖 precheck 覆盖 | +| **precheck** | 有(`[context-overflow-precheck]` + `route=compact_then_truncate`) | ✅ 含 | 有 | **直接检测** | +| **threshold** | 无(静默执行) | — | 有 | counter+lock+status 保护 | +| **manual** | 无(静默执行) | — | 有 | counter+lock+status 保护 | + +### 0.3 为什么只依赖 precheck 标记 + +1. **overflow/timeout 标记不含 sessionKey**:实测证实 overflow 标记(`context overflow detected; attempting auto-compaction for zhipu/glm-5.1`)不包含 `agent:xxx:main` 格式的 sessionKey,被前置 `session_key not in msg` 过滤跳过,是死代码。 +2. **precheck 总在 overflow 之前触发**:同一 compact 事件中,precheck `route=compact_then_truncate` 先检测到,overflow 是 fallback。所以 precheck 已覆盖 overflow 场景。 +3. **threshold/manual 无开始标记**:这两种是静默执行,没有 gateway 日志标记。它们依赖 counter+lock+status 三重保护(见 §07),不需要 gateway 日志检测。 + +> **注意**:`route=truncate_tool_results_only` 的 precheck 不触发 compact 检测,只有 `route=compact_then_truncate` 才触发。 + +### 0.4 超时兜底 + +15 分钟超时窗口:如果 compact 开始标记超过 15 分钟仍无结束标记,自动忽略该开始标记。这覆盖了: +- daemon 重启后残留的开始标记 +- 极端长时间的 compact(正常 compact 通常 < 7 分钟) +- 日志轮转导致的结束标记丢失 + +--- + ## 1. 问题 ### 1.1 现象 @@ -257,3 +299,6 @@ trajectory jsonl 路径 = `{sessionFile}.trajectory.jsonl`,其中 sessionFile - **v2**:gateway 日志 precheck 开始标志 → 仲达指出开始标志覆盖率仅 30%,建议 rotation-only - **v3**:rotation-only + 120s 窗口 → 合并 PR #36,但实测 51 分钟 compact loop 无法覆盖 - **v4**:trajectory prompt.submitted → 仲达背靠背验证(源码 7 条 skipPromptSubmission 路径 + 实际数据 ~8% 假阳性但方向安全)→ 修正检测目标为"session 是否正常" +- **v5**:gateway log(precheck 开始标记)+ jsonl(compaction 结束标记)配对 → 仲达评审通过后实现,PR #48 Review 驳回 M1/M2 修正后合并 + +> ⚠️ **v4 已 deprecated**。v4 的 trajectory prompt.submitted 方案未实施,最终实施的是 v5。v4 的分析(skipPromptSubmission 路径、实测数据)仍有参考价值。 diff --git a/src/daemon/spawner.py b/src/daemon/spawner.py index 12c0e96..a83170f 100644 --- a/src/daemon/spawner.py +++ b/src/daemon/spawner.py @@ -1388,13 +1388,18 @@ curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_ agent_id: str, window_seconds: int = 900) -> Optional[str]: """v5: 检查 gateway 日志,找最近的 compact 开始标记。 - 开始标记匹配规则(message 字段包含以下任一): - - "attempting auto-compaction" (overflow 路径) - - "[timeout-compaction]" 且包含 "attempting" (timeout 路径) - - "[context-overflow-precheck]" 且 "route=compact_then_truncate" (precheck 路径) + 只检测 precheck 路径:message 含 "[context-overflow-precheck]" 且 + "route=compact_then_truncate"。原因: + - overflow 标记("attempting auto-compaction")不含 sessionKey, + 被 `session_key not in msg` 前置过滤跳过,是死代码。 + - timeout 标记推测同理不含 sessionKey。 + - precheck 标记含 sessionKey 且实测总在 overflow 之前触发(同一 compact + 事件,precheck 先检测到,overflow 是 fallback),所以 precheck 已覆盖 + overflow 场景。 + - threshold/manual 触发的 compact 无开始标记(静默执行),依赖 + counter+lock+status 保护,不需要 gateway 日志检测。 - 同时需要包含目标 agent 的 sessionKey(如 agent:simayi-challenger:main)。 - 超时兜底:开始标记超过 window_seconds 自动忽略。 + 超时兜底:开始标记超过 window_seconds(默认 15 分钟)自动忽略。 返回最近一个开始标记的 UTC ISO 时间字符串(带 Z 后缀),或 None。 """ @@ -1434,20 +1439,10 @@ curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_ if session_key not in msg: continue - # 检测三种开始标记 - is_start = False - # overflow: "attempting auto-compaction" - if "attempting auto-compaction" in msg: - is_start = True - # timeout: "[timeout-compaction]" + "attempting" - elif "[timeout-compaction]" in msg and "attempting" in msg: - is_start = True - # precheck: "[context-overflow-precheck]" + "route=compact_then_truncate" - elif ("[context-overflow-precheck]" in msg - and "route=compact_then_truncate" in msg): - is_start = True - - if not is_start: + # 只检测 precheck 路径:route=compact_then_truncate + # overflow/timeout 标记不含 sessionKey,被前置过滤跳过(死代码),已删除 + if ("[context-overflow-precheck]" not in msg + or "route=compact_then_truncate" not in msg): continue # 解析时间