--- name: Context Compressor slug: context-compressor version: 5.0.0 description: 自动上下文压缩。当session超阈值时,先提取重要内容到日记,再压缩JSONL。支持light/standard/aggressive模式。触发条件:session>8K tokens、用户要求压缩、心跳维护时发现session过大。v5.0新增:防死循环、大输出截断、性能优化。 metadata: {"openclaw":{"emoji":"📦","requires":{"bins":[]},"os":["linux","darwin","win32"]}} --- # Context Compressor 📦 **两阶段压缩:先提取,再压缩。不提取就压缩 = 丢信息。** ## 何时压缩 | 条件 | 动作 | |------|------| | Session > 8K tokens(~32K字符) | 主动压缩 | | 用户说"压缩上下文"/"compress context" | 立即压缩 | | 心跳检查发现session膨胀 | 主动压缩 | | 长任务开始前,session已很大 | 先压缩再继续 | **不要压缩**:任务进行中、session即将结束、刚压缩过且仍低于2倍阈值。 ## Token估算 | 内容 | 公式 | 说明 | |------|------|------| | 主要中文 | `字符数 / 1.5` | 中文token密度高 | | 主要英文 | `字符数 / 4` | 英文token密度低 | | 混合 | `字符数 / 3` | 通用估算(脚本默认) | | JSON/代码多 | `字符数 / 3.5` | 结构化内容更紧凑 | **注意**:以上为粗略估算,实际token数因模型而异。建议用模型官方tokenizer精确计算。 **快速判断**:session文件 > 32K字符 ≈ > 8K tokens(混合内容),该压缩了。 --- ## Phase 1: 提取(必须先做) 扫描session中间部分,把重要内容存到外部文件。 | 要提取的 | 存到哪里 | 格式 | |---------|---------|------| | 用户偏好/事实 | SOUL.md → 偏好部分 | 更新或新增bullet | | 关键决策/结论 | memory/YYYY-MM-DD.md | `## Decision: ` | | 创作作品全文 | memory/YYYY-MM-DD.md | `## Creative: ` | | 重要对话摘录 | memory/YYYY-MM-DD.md | `## Conversation: <topic>` | | 技术解决方案 | memory/YYYY-MM-DD.md | `## Technical: <issue>` | | 新话题/类别 | SOUL.md → 事件索引 | 新增条目 | | 闲聊/日常 | **不存** | — | **日记追加格式**: ```markdown --- ## 压缩提取 HH:mm ### <Category>: <Brief Title> - **Context**: 为什么重要 - **Content**: 实际信息 - **Session ref**: 大致轮次范围 ``` **日记追加,绝不覆盖。** --- ## Phase 2: 压缩Session ### 优先级系统(从低到高删) | 优先级 | 内容 | 删减顺序 | |--------|------|---------| | P0 | 无法解析的行(安全保留) | 保留 | | P1 | 大型tool输出(>2000字符) | 1st | | P2 | 导航/探索调用(ls, pwd, grep未命中) | 2nd | | P3 | 成功但常规的tool调用 | 3rd | | P4 | 冗长的assistant解释 | 4th | | P5 | 日常寒暄("好的""谢谢") | 5th | | P6 | 有意图的用户消息 | 保留 | | P7 | 最近对话(tail) | 保留 | | P8 | System消息 | 保留 | | P9 | 已有Summary(合并处理) | 合并 | | P10 | Session header | 保留 | ### 压缩模式 | 模式 | Token目标 | 操作 | 适用场景 | |------|----------|------|---------| | light | 6K | 只删P1-P2 | 刚过阈值,主要是tool膨胀 | | standard | 4K | 删P1-P3,摘要P4-P5 | 常规维护(默认) | | aggressive | 2.5K | 删P1-P5,最大化摘要 | session很长,需要最大压缩 | ### 大输出截断策略 当tool输出 >2000字符时,不要只做全删/全留二选一。优先截断: | 策略 | 操作 | 节省 | |------|------|------| | 截断到500字符 | 保留前500字符 + `...[truncated, N chars omitted]` | ~75% | | 截断到200字符 | 保留前200字符 + 截断标记 | ~90% | | 全删 | 完全移除该行 | 100% | **截断优先级**:先截断到500 → 仍超标则截断到200 → 仍超标则全删。 截断时保留tool调用的结构信息(tool name、参数摘要),只删输出内容。 ### 操作步骤 ``` 1. 读取 session JSONL 路径: ~/.openclaw-autoclaw/agents/<agentId>/sessions/<sessionId>.jsonl 2. 估算token数,低于阈值则退出 3. 检查防死循环标记(见下方),如触发则跳过 4. 备份原文件 → <sessionId>.jsonl.bak 5. 分类每行(P0-P10),缓存解析结果,避免重复JSON解析 6. 识别tail(从末尾往前数60行P6+内容) 7. 截断大输出(P1行先截断到500字符,再截断到200,最后全删) 8. 按优先级从低到高删除(不删tail) 直到token数达标或只剩P6+ 每删一行计数+1,超过maxIter则停止 9. 生成 [SESSION SUMMARY](合并已有摘要,不要堆叠) ≤6条bullet,每条≤2行,带文件引用 10. 组装:header + summary(1行) + tail 11. 验证:每行有效JSON、至少1条user+1条assistant、token数达标 12. 写入,验证通过后删.bak,失败则回滚 13. 写入防死循环标记 14. 日志:追加到 memory/YYYY-MM-DD.md ``` ### Summary格式 ``` [SESSION SUMMARY] • [Decision] <决定内容> → memory/<YYYY-MM-DD>.md##Section • [Technical] <解决方案> → memory/<YYYY-MM-DD>.md##Section • [Fact] <关键事实> • [State] <当前任务状态> • [Warning] <未来必须知道的事> ``` **规则**:≤6条、每条≤2行、带文件引用、用标签分类、只写结果不写过程。日期使用当天日期。 ### 处理已有Summary 如果session中已有 `[SESSION SUMMARY]`: 1. 解析旧summary 2. 与新内容合并 3. 去重:`[State]`替换为新`[State]`,已解决的`[Warning]`删除,同类多bullet保留(如多个`[Technical]`) 4. 结果始终只有一条 `[SESSION SUMMARY]` **绝对不要堆叠多条summary。** --- ## 防死循环机制 ⚠️ 本地模型执行压缩时可能陷入死循环。以下机制防止此问题: ### 1. 最大迭代限制 | 参数 | 默认值 | 说明 | |------|--------|------| | maxIter | 100 | 模型内操作时,删除操作的最大迭代次数 | | maxIter (脚本) | 5000 | 脚本执行时的最大迭代次数 | 超过限制后立即停止,输出当前结果。 ### 2. 压缩冷却期 压缩完成后,在session中写入标记: ```json {"type":"system","lastCompressed":"<timestamp>","compressedTokens":<N>} ``` **规则**: - 距上次压缩 < 5分钟 → 不压缩 - 上次压缩后token数未增长 > 20% → 不压缩 - 连续3次压缩后仍未达标 → 停止压缩,报告错误 ### 3. 压缩效果检查 压缩后验证: - 压缩后token数必须 < 压缩前token数 × 0.9(至少减少10%) - 如果压缩后几乎没减少 → 说明session主要由P6+内容组成,无法进一步压缩 - 此时不要重试,而是报告:"Session主要由高优先级内容组成,无法进一步压缩。建议增大阈值或手动清理。" ### 4. 模型执行安全 当由本地模型执行压缩时: - **单次执行**:每次只执行一次完整的压缩流程 - **不自动重试**:压缩完成后不自动检查是否需要再次压缩 - **明确退出**:压缩完成后输出 `"Compression complete. <N> tokens remaining."` 然后停止 - **异常中断**:如果执行过程中遇到错误,立即停止并报告,不要尝试恢复 --- ## 安全与恢复 | 步骤 | 动作 | |------|------| | 压缩前 | 复制 `.jsonl` → `.jsonl.bak` | | 验证通过 | 删除 `.jsonl.bak` | | 验证失败 | 回滚 `.jsonl.bak` → `.jsonl` | | 手动恢复 | 用户可从 `.jsonl.bak` 恢复 | 验证清单:每行有效JSON ✅ 至少1条user消息 ✅ 至少1条assistant消息 ✅ token数达标 ✅ --- ## 脚本(可选) 如需脚本化执行,见 `scripts/` 目录: | 脚本 | 平台 | 用法 | |------|------|------| | `compress-session.ps1` | Windows | `.\compress-session.ps1 -SessionFile <path> [-Mode standard] [-KeepTail 60] [-MaxIter 100] [-DryRun]` | | `compress-session.sh` | Linux/macOS | `./compress-session.sh <session-file> [mode] [keep-tail] [dry-run]` | | `compress-simple.py` | 跨平台 | `python compress-simple.py <session.jsonl> [mode] [tail_lines]` | | `compress-all.py` | 跨平台(批量) | `python compress-all.py` | | `diary-logger.py` | 跨平台 | 见下方日记记录章节 | **模型内操作优先**:直接用read/write/exec工具按上述步骤执行,比脚本更灵活。 --- ## 日记记录 ### diary-logger.py 用法 ```bash # Phase 1: 压缩前提取重要内容到日记 python diary-logger.py extract <session.jsonl> <workspace_dir> # 压缩后记录压缩事件 python diary-logger.py log-compression <workspace_dir> --mode standard --before 8000 --after 4000 --removed 45 # 验证日记完整性 python diary-logger.py validate <workspace_dir> # 手动更新 MEMORY.md 索引 python diary-logger.py update-index <workspace_dir> --note "手动添加的备注" ``` ### 日记记录触发机制 | 触发时机 | 动作 | 命令 | |---------|------|------| | 压缩前(Phase 1) | 提取重要内容到日记 | `diary-logger.py extract` | | 压缩后 | 记录压缩事件 | `diary-logger.py log-compression` | | 心跳检查时 | 验证日记完整性 | `diary-logger.py validate` | | 手动 | 更新MEMORY.md索引 | `diary-logger.py update-index` | ### 日记数据格式 压缩提取追加到 `memory/YYYY-MM-DD.md`: ```markdown --- ## 压缩提取 HH:mm ### Decision: <简要标题> - **Context**: 为什么重要 - **Content**: 实际信息 - **Source**: user/assistant/summary ``` 压缩事件追加到 `memory/YYYY-MM-DD.md`: ```markdown --- ## 压缩执行 HH:mm ### Compression: standard mode - **Before**: 8,000 tokens - **After**: 4,000 tokens - **Saved**: 50.0% - **Removed lines**: 45 - **Timestamp**: 2026-04-21T12:00:00+08:00 ``` ### 错误处理策略 | 错误场景 | 处理方式 | |---------|---------| | 日记文件不存在 | 自动创建,含正确标题 | | 写入失败 | 从 .bak 回滚,报告错误 | | MEMORY.md 格式异常 | 前缀匹配日期,兼容 `### 2026-04-21 — 标题` 格式 | | 提取无重要内容 | 输出提示,不创建空条目 | | .bak 文件残留 | validate 命令检测并报告 | ### 完整性验证 `diary-logger.py validate` 检查以下项目: 1. ✅ 今日日记文件存在 2. ✅ 日记标题格式正确(`# YYYY-MM-DD`) 3. ✅ 日记包含章节内容 4. ✅ MEMORY.md 包含今日条目 5. ✅ SOUL.md 存在 6. ✅ memory/ 目录存在且有文件 7. ✅ 无残留 .bak 文件 8. ⚠️ 检测空章节 9. ⚠️ 检测压缩提取次数过多(>3次) --- ## 配置 | 参数 | 默认值 | 说明 | |------|--------|------| | tokenThreshold | 8000 | 超过此token数触发压缩 | | charThreshold | 32000 | 备用:超过此字符数触发 | | keepTail | 60 | 保留最近对话的最小行数 | | mode | standard | 压缩模式:light/standard/aggressive | | autoBackup | true | 压缩前自动备份 | | maxIter | 100 | 模型内操作最大迭代次数 | | truncateThreshold | 2000 | 超过此字符数的tool输出触发截断 | | truncateSize | 500 | 截断后保留的字符数 | | cooldownMinutes | 5 | 两次压缩之间的最小间隔(分钟) | --- ## 最佳实践 1. **先提取再压缩** — 不提取就压缩 = 永久丢失信息 2. **按token不是按行** — 100行短对话 ≠ 100行tool输出 3. **先截断再删除** — 截断大输出比全删更安全,保留上下文线索 4. **先删tool调用** — 最低价值、最高token,通常单这一步就能省20-40% 5. **只有一条summary** — 合并不堆叠 6. **summary里引用外部文件** — 让未来的自己能找到详情 7. **不中途压缩** — 等任务边界再压缩 8. **遵守冷却期** — 不要连续压缩,至少间隔5分钟 9. **日志记录** — 压缩后追加到日记:`"Compressed X→Y tokens (mode)"` 10. **压缩失败就停止** — 不要重试,报告错误让用户决定