Skill: lanyasheng/eh-context-memory (version 2.4.0)

--- name: context-memory version: 2.0.0 description: 上下文窗口管理与跨 session 知识传递。当需要跨阶段传递决策、压缩前抢救知识时使用。不适用于工具重试（用 tool-governance）或多 agent 协调（用 multi-agent）。参见 execution-loop（阶段边界）。 license: MIT triggers: - context management - 上下文管理 - handoff document - compaction - token budget - memory consolidation - context estimation author: OpenClaw Team --- # Context & Memory 上下文窗口生命周期管理：跨阶段知识传递、压缩前知识抢救、token 预算。 ## When to Use - 多阶段任务跨阶段传递决策 → Handoff documents - 即将压缩需要保存知识 → Compaction memory extraction - 需要监控 context 使用率 → Context budget estimation ## When NOT to Use - Agent 提前停止 → 用 `execution-loop` - 多 agent 协调 → 用 `multi-agent` --- ## Patterns ### 3.1 Handoff 文档 [design] 阶段结束或压缩前，将五段结构写入 `sessions/<session-id>/handoffs/stage-N.md`： ```markdown ## Decided - 选用 Redis Cluster 6 节点，hash slot 自动分片 ## Rejected - Codis（社区停更，Proxy 延迟 +2ms） - Sentinel（不支持分片，只做主从故障转移） ## Risks - 节点间 gossip 协议在跨机房部署时延迟可能超过心跳超时 ## Files Modified - config/redis.yaml, src/cache/cluster.ts ## Remaining - 压测 6 节点写入 QPS 是否满足峰值要求 ``` 五段缺一不可。压缩摘要由 LLM 决定保留什么，handoff 由你决定——信息在磁盘上，任何级别的 compact 都不会丢失。注入方式：下个 agent prompt 中加 `once: true` 引用此文件。 → [详见](references/handoff-documents.md) ### 3.2 Compaction 前记忆提取 [script] Claude Code 没有 PreCompact hook。通过 Stop hook 每 N 轮（`COMPACTION_EXTRACT_INTERVAL` 环境变量，默认 15 轮）定期快照关键决策到磁盘。 与 handoff 互补：handoff 是计划内的阶段传递，compaction 提取是应急的自动抢救。Claude Code 内部 4 级 compaction：MicroCompact（10-50K tokens，免费）、Session Memory（60-80%，免费）、Full Compact（80-95%，1 次 API 调用）、Reactive Compact（413 触发，紧急）。 → [详见](references/compaction-extract.md) ### 3.3 三门控记忆合并 [design] 跨 session 积累的 handoff 文件会碎片化、重复、矛盾。三门控按计算成本从低到高排列，任一失败即跳过： 1. **Time gate**: 距上次合并 >= 24 小时 2. **Session gate**: 累积 >= 5 个新 session 的 handoff 3. **FileLock gate**: 获取文件锁（10 次重试，5-100ms 指数退避） 通过后合并逻辑：按时间排序所有 handoff，提取 Decided/Rejected，去重，后来的决策覆盖早期的（如 stage-3 否决了 stage-1 选的方案，以 stage-3 为准）。 → [详见](references/memory-consolidation.md) ### 3.4 Token 预算分配 [design] UserPromptSubmit hook 估算 context 使用量，按阈值梯度注入行为指令（基于 200K window）： | Context 使用率 | 行为指令 | |---------------|----------| | < 40% | 自由读取文件 | | 40-60% | 警告，优先使用 grep | | 60-80% | 优先 grep/subagent，避免读大文件 | | 80-95% | 必须使用 subagent，禁止直接读文件 | | >= 95% | 必须允许 agent 停止（否则 context 溢出崩溃） | Hook 输出格式：`{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"[CONTEXT BUDGET] 当前 ~150K/200K (75%), 优先用 grep 和 subagent"}}`。 前半段消耗过多 context 导致后半段被迫压缩是常见失败模式。 → [详见](references/token-budget.md) ### 3.5 Context Token 估算 [script] 从 transcript JSONL 尾行提取 token 数：`tail -1 "$TRANSCRIPT" | jq -r '.usage.input_tokens // empty'`。 关键限制： - transcript 不暴露 `context_window_size`（该字段仅通过 statusLine stdin pipe 提供给 HUD），只能拿原始 token 数 - 需过滤 stream placeholder 行（`input_tokens <= 10`） - 阈值判断基于 200K window 假设 - 精度粗略：bytes/4 估算可偏差 20-30%，Claude Code 内部用 `Math.ceil(totalTokens * (4/3))` 做 33% padding → [详见](references/context-usage.md) ### 3.6 文件系统作工作记忆 [design] `.working-state/` 目录下两个核心文件： - `current-plan.md` — 当前计划，随时覆写更新 - `decisions.jsonl` — 决策日志，append-only，每行一个 JSON： ```json {"ts": "2025-03-15T10:30:00Z", "decision": "选用 Redis Cluster", "reason": "需要分片能力", "alternatives": ["Codis", "Sentinel"]} ``` compact 或 crash 后 agent 读这两个文件恢复状态。需要在 prompt 中明确要求 agent 写入。恢复指令："read `.working-state/current-plan.md` and `.working-state/decisions.jsonl` first"。 → [详见](references/filesystem-working-memory.md) ### 3.7 Compaction 质量审计 [design] compact 后对照 `decisions.jsonl` 检查 compact summary 中是否保留了关键决策： 1. 读 `decisions.jsonl` 最近 5 条，提取关键词 2. grep compact summary（`tail -c 4096 "$TRANSCRIPT"`）查这些关键词 3. 缺失 → 注入：`{"decision":"allow","hookSpecificOutput":{"additionalContext":"[COMPACT AUDIT] 以下决策在压缩中丢失: ..."}}` 关键词匹配是粗糙近似。漏检代价（方向倒退）远大于误检代价（多注入几条）。 → [详见](references/compaction-quality-audit.md) ### 3.8 Auto-Compact 断路器 [design] `MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3`。连续 3 次 auto-compact 失败后停止尝试，等 Reactive Compact（API 413 触发）兜底。 递归保护：如果 compact 来源是 `'session_memory'`、`'compact'` 或 `'marble_origami'`，跳过 auto-compact。历史数据：1,279 个 session 出现 50+ 次连续失败，浪费约 250K API 调用/天。hook 开发者需处理 circuit breaker 跳过的情况。 → [详见](references/auto-compact-circuit-breaker.md) ## Scripts | 脚本 | 用途 | |------|------| | `context-usage.sh <transcript>` | 从 transcript 尾行提取 input_tokens（原始数，非百分比） | | `compaction-extract.sh` | Stop hook 定期触发，提取关键决策到 handoff | ## Workflow ``` 阶段结束？ → 写 handoff（Decided/Rejected/Risks/Files/Remaining 五段，缺一不可） → 不确定是否阶段边界？查任务清单或问用户 Context > 80%？ → 运行 compaction-extract.sh 抢救关键决策到磁盘 → 注入预算指令：禁止读大文件，委托 subagent Compact 刚发生？ → 审计存活：对照 decisions.jsonl 检查 compact summary → 发现遗失 → 自动注入缺失决策 → 连续失败 3 次 → circuit breaker 生效，等 Reactive Compact 兜底 ``` <example> 场景: Handoff 保护决策不丢失 任务: 20 轮 Redis 方案讨论 第 16 轮: 定方案 Redis Cluster 6 节点，否决 Codis（社区停更）和 Sentinel（不支持分片） 第 17 轮: 写 handoff 到 sessions/xxx/handoffs/stage-2.md（Decided/Rejected/Risks/Files/Remaining 五段） 第 20 轮: Full Compact 截断 context 结果: Compact 后 agent 读 stage-2.md，恢复全部决策，不会重提 Codis </example> <example> 场景: Compaction 质量审计发现决策丢失 前提: decisions.jsonl 最近 5 条含 "Redis Cluster"、"Codis"、"Sentinel" 触发: Full Compact 后审计 hook 启动 检测: grep compact summary（tail -c 4096），"Codis" 未出现 动作: 注入 additionalContext "[COMPACT AUDIT] 丢失决策: 否决 Codis（社区停更，Proxy +2ms）" 结果: Agent 读到注入信息，不会再提议 Codis </example> <example> 场景: Token 预算阻止 context 溢出 状态: 第 30 轮，UserPromptSubmit hook 检测 input_tokens = 170K 计算: 170K / 200K = 85%，超过 80% 阈值 注入: "[CONTEXT BUDGET] 85%, 必须使用 subagent，禁止直接读大文件" 结果: Agent 切换 subagent 委派，避免第 35 轮溢出触发紧急 Reactive Compact </example> <anti-example> 同样 20 轮讨论，没写 handoff。第 20 轮 Compact 后 agent 丢失 Codis 被否决的原因，重新提议"考虑 Codis？"。用户花 3 轮重复解释——浪费 token，决策质量倒退。 </anti-example> ## Output | 产物 | 路径 | 说明 | |------|------|------| | 阶段 handoff 文件 | `sessions/xxx/handoffs/stage-N.md` | 每个阶段边界写一份，包含 Decided/Rejected/Risks/Files/Remaining | | 压缩抢救文件 | `compaction-extract.json` | 压缩前自动提取的关键决策和否决记录 | | context 使用率估算 | `context-usage.sh` 输出 | 当前 token 占比、预警阈值、剩余容量估算 | ## Usage 在 `.claude/settings.json` 中配置 hook： ```jsonc { "hooks": { // Token 预算注入（Pattern 3.4） "UserPromptSubmit": [ { "hooks": [{ "type": "command", "command": "bash context-usage.sh \"$TRANSCRIPT_PATH\"" }] } ], // 定期快照关键决策（Pattern 3.2） "Stop": [ { "hooks": [{ "type": "command", "command": "bash compaction-extract.sh", "async": true }] } ] } } ``` `context-usage.sh` 输出格式： ```json {"decision": "allow", "hookSpecificOutput": {"additionalContext": "[CONTEXT BUDGET] ~150K/200K (75%). 优先用 grep 和 subagent，避免直接读大文件。"}} ``` `compaction-extract.sh` 每 15 轮触发一次（`COMPACTION_EXTRACT_INTERVAL` 环境变量控制），将 `decisions.jsonl` 最近条目写入 handoff 文件。 ## Related - `execution-loop` — 阶段边界信号触发 handoff 写入（Pattern 1.4 task completion 完成时即为阶段边界） - `multi-agent` — 跨 agent 任务交接时，handoff 文件作为知识传递载体（替代口头 context 传递） - `error-recovery` — crash 恢复时读取最近的 handoff 文件重建进度（Pattern 5.2 crash state recovery）

path: metadata.json
content: name: context-memory
version: 2.0.0
description: Context window management and knowledge transfer
license: MIT

path: references/auto-compact-circuit-breaker.md
content: # Pattern 3.8: Auto-Compact Circuit Breaker

## Problem

Auto-compact 触发后可能因各种原因失败（API 413、LLM 生成的摘要不合法、网络超时）。如果失败后立即重试，可能陷入"compact 失败 → 重试 → 又失败"的循环，每次重试都消耗一次 API 调用。

Claude Code 内部数据：曾有 1,279 个 session 出现 50 次以上连续 compact 失败，浪费约 250K API 调用/天。

## Solution

Claude Code 内置了 circuit breaker：`MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3`。连续 3 次 auto-compact 失败后停止尝试，等待 Reactive Compact（由 API 413 错误触发）作为最终兜底。

这和 M3 Safety Valves 原则直接对应——压缩机制也需要逃生条件。

## 对 Hook 开发者的意义

如果你的 hook 依赖 compact 事件（比如 Pattern 3.7 Compaction Quality Audit 在 compact 后跑审计），需要知道：

1. Compact 不一定每次都成功。你的 hook 必须处理"compact 被 circuit breaker 跳过"的情况。
2. 触发阈值是 3 次。如果你看到 context 持续膨胀但没有 compact 发生，可能是 circuit breaker 生效了。
3. Reactive Compact 是最终兜底——但它只在 API 返回 413 时触发，可能比 auto-compact 晚很多。

## 递归保护

Auto-compact 还有递归保护：如果当前查询来自 `'session_memory'`、`'compact'` 或 `'marble_origami'` source，跳过 auto-compact 检查。防止 compact 的 LLM 调用本身触发新的 compact。

## Source

Claude Code 源码 `autoCompact.ts`（352 行），`MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES` 常量。


path: references/compaction-extract.md
content: # Pattern 3.2: Compaction 前记忆提取（Save Before Delete）

## 问题

Claude Code 的 auto-compact 在 context 接近上限时自动触发。压缩会丢弃旧消息，其中可能包含重要的设计决策和发现。

## 原理

利用 Stop hook 定期提取关键信息到磁盘。注意：Claude Code **没有** PreCompact hook event。实际实现通过 Stop hook 每 N 轮触发一次快照，作为压缩前的预防性知识保存。Claude Code 内部有 `buildExtractAutoOnlyPrompt` 和 `buildExtractCombinedPrompt`，但这些是内部 API，不暴露给外部 hook。

## 与 Handoff 文档的区别

| | Handoff 文档 (Pattern 3.1) | Compaction 提取 (Pattern 3.2) |
|---|---|---|
| 触发时机 | 阶段结束时（主动） | 压缩触发时（被动） |
| 触发者 | Agent 自行写入 | Stop hook 定期触发 |
| 内容控制 | 完全由 agent 决定 | 由脚本自动快照 |
| 可靠性 | 依赖 agent 遵守指令 | 系统级定期保证 |

两者互补：handoff 是计划内的上下文传递，compaction 提取是应急的知识抢救。

## 实现

settings.json 中配置 Stop hook（每 N 轮触发一次快照）：

```json
{
  "hooks": {
    "Stop": [{
      "hooks": [{
        "type": "command",
        "command": "bash /path/to/context-memory/scripts/compaction-extract.sh",
        "async": true
      }]
    }]
  }
}
```

通过 `COMPACTION_EXTRACT_INTERVAL` 环境变量控制快照频率（默认每 15 轮）。

> **注意**: Claude Code 没有 PreCompact hook event。此脚本通过定期快照实现预防性知识保存，而非精确的压缩前拦截。

## Claude Code 的 Memory Extraction 机制

Claude Code 内部有两种 extraction prompt builder：

- `buildExtractAutoOnlyPrompt`：只提取 private memory（用户偏好、工作习惯）
- `buildExtractCombinedPrompt`：根据内容敏感性路由到 private 或 team scope

提取发生在压缩的同时（并行），提取结果写入 memdir 的 Markdown 文件。Memory taxonomy 包含 4 种类型：user（个人偏好）、feedback（纠正/表扬）、project（架构/目标）、reference（外部文档/流程）。


path: references/compaction-quality-audit.md
content: # Compaction Quality Audit

## Problem

Claude Code 的自动 compact 会丢失关键上下文。Compact 后 agent 可能忘记：已做的决策（重复讨论）、已排除的方案（又去尝试）、文件间的依赖关系（改了 A 忘了同步 B）。用户发现问题时往往已经浪费了大量 token 在错误方向上。

## Solution

Compact 完成后立即运行审计脚本，检查关键上下文是否在 compact summary 中存活。审计对照 `.working-state/decisions.jsonl` 中的关键决策和 `.harness-tasks.json` 中的任务状态，如果发现遗失则将缺失信息注入 context。

## Implementation

1. 通过 UserPromptSubmit hook 检测 compact 事件（transcript 中出现 compact 标记）
2. 读取 compact 后的 summary（transcript 尾部）
3. 对照 `.working-state/decisions.jsonl` 的最近 N 条决策
4. 检查关键信息是否在 summary 中出现

```bash
INPUT=$(cat)
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // ""')
[ -z "$TRANSCRIPT" ] && exit 0

# 检测是否刚发生 compact（简化检测：看 transcript 大小是否骤降）
DECISIONS_FILE=".working-state/decisions.jsonl"
[ -f "$DECISIONS_FILE" ] || exit 0

# 读取最近 5 条决策的关键词
KEYWORDS=$(tail -5 "$DECISIONS_FILE" | jq -r '.decision' | tr ' ' '\n' | sort -u | head -20)

# 读取 transcript 尾部（compact summary 区域）
SUMMARY=$(tail -c 4096 "$TRANSCRIPT" 2>/dev/null)

MISSING=""
for KW in $KEYWORDS; do
  if ! echo "$SUMMARY" | grep -qi "$KW" 2>/dev/null; then
    MISSING="${MISSING}${KW}, "
  fi
done

if [ -n "$MISSING" ]; then
  RESTORE=$(tail -5 "$DECISIONS_FILE")
  cat <<EOF
{"decision":"allow","hookSpecificOutput":{"additionalContext":"[COMPACT AUDIT] 以下关键决策可能在 compact 中丢失，重新注入：\n${RESTORE}\n请在继续工作前确认这些决策仍然有效。"}}
EOF
fi
```

5. 更可靠的做法：compact 前主动将关键 context 写入 `.working-state/pre-compact-snapshot.md`，compact 后读回

## Tradeoffs

- **Pro**: 防止 compact 导致的决策遗忘和方向倒退
- **Pro**: 自动化——不需要用户手动检查 compact 质量
- **Con**: 关键词匹配是粗糙的近似——compact 可能用同义词表达了同一决策
- **Con**: 注入恢复信息会占用 compact 后"腾出来"的 context 空间

## Source

Claude Code auto-compact 行为观察。OMC 的 post-compact file restoration 机制（compact 后恢复 top 5 文件，50K token 预算）。


path: references/context-usage.md
content: # Pattern 3.5: 轻量 Context 使用量估算

## 问题

需要知道 agent 的 context window 用了多少，但 transcript JSONL 文件可能 100MB+，不能全部读取。

## 原理

Claude Code 的 transcript 是 append-only JSONL。最新的 API 响应在文件末尾，包含 `usage.input_tokens` 字段。读取最后一行即可提取当前 input token 数。

> **重要限制**：Claude Code 的 transcript JSONL **不包含** `context_window_size` 字段。该数据仅通过 statusLine stdin pipe 提供给 HUD 插件。因此，hook 脚本**无法计算 context 使用百分比**，只能获取原始 input token 数。

来自 Claude Code 内部的 HUD 实现（通过 stdin pipe 读取 `context_window.context_window_size` 和 `context_window.current_usage.input_tokens`）和 OMC 的 `context-guard-stop.mjs`。

## 实现（`scripts/context-usage.sh`）

```bash
TRANSCRIPT="$1"
[ -f "$TRANSCRIPT" ] || exit 0

# 读取最后一行（不是 tail -c 4096，因为真实 JSONL 行可达 20KB+）
LAST_LINE=$(tail -1 "$TRANSCRIPT" 2>/dev/null)
[ -z "$LAST_LINE" ] && exit 0

# 从嵌套的 usage 对象中提取 input_tokens
# 真实格式："usage":{"input_tokens":N,"cache_creation_input_tokens":0,...}
INPUT_TOKENS=$(echo "$LAST_LINE" | jq -r '.usage.input_tokens // empty' 2>/dev/null)

# 过滤流式占位符（input_tokens <= 10 是流式中间值）
if [ -n "$INPUT_TOKENS" ] && [ "$INPUT_TOKENS" -gt 10 ]; then
  echo "Input tokens: ${INPUT_TOKENS}"
fi
```

## 用途

1. **Token 预算管理**：通过原始 token 数做粗略阈值判断（如 >160K 告警，基于 200K context window 假设）
2. **Hook pair bracket 中的增量追踪**：每轮记录 token 增量（见 Pattern 6.3）
3. **外部监控**：周期性检查所有运行中 session 的 token 消耗

## 为什么不能算百分比

| 数据 | 来源 | Hook 脚本可访问 |
|------|------|----------------|
| `input_tokens` | transcript JSONL 的 `usage` 对象 | 是 |
| `context_window_size` | statusLine stdin pipe | 否 |
| `used_percentage` | statusLine stdin pipe | 否 |

HUD 插件（如 claude-hud）可以算百分比，因为它通过 stdin pipe 获取完整数据。Hook 脚本只能拿到 transcript。

## Claude Code 的 token 估算精度（来自源码分析 v2.1.88）

Claude Code 内部使用 3 级精度估算：

- **粗估**：bytes / 4（零成本，毫秒级）
- **代理**：Haiku input count（便宜但需要 API 调用）
- **精确**：countTokens API（慢但准确）

粗估还加 33% 保守缓冲：`Math.ceil(totalTokens * (4/3))`。transcript 尾部读取得到的是 API 返回的实际值，精度等同于精确级。


path: references/extended-patterns.md
content: # Context Memory — Extended Patterns

### E3.1: 4-Tier Compression Pipeline
**What it does**: Context 压缩分 4 个层级，按粒度从小到大依次触发：(1) micro-compact（工具输出截断，单条消息级别）；(2) session-compact（会话内压缩，合并相邻消息）；(3) full-compact（完整摘要，保留关键决策和结论）；(4) reactive-compact（紧急压缩，context 到 95% 时触发，激进丢弃）。每层有独立的触发阈值和保留策略。
**Why it matters**: 一刀切的 compact 要么太激进（丢了关键信息）要么太保守（压缩效果不够）。分层压缩让小问题（单个大输出）在低层解决，不触发全局压缩。只有累积性的 context 增长才逐级向上触发更激进的压缩。
**Source evidence**: Claude Code 内部的 auto-compact 逻辑中有 micro（工具输出截断到 N chars）和 full（整体 summary）两层。reactive compact 在 95% 阈值触发。

### E3.2: Auto-Compact Circuit Breaker
**What it does**: 跟踪连续 compact 失败次数（`MAX_CONSECUTIVE_FAILURES=3`）。如果 compact 后 context 没有有效减少（压缩率 < 10%），计为失败。连续 3 次失败后停止自动 compact，转为通知用户手动处理。
**Why it matters**: 当 context 中的内容全是"不可压缩"的（密集代码、长日志），反复 compact 只消耗 API 调用但不释放空间，还可能丢失关键信息。Circuit breaker 防止无效 compact 循环——在明知无效时尽早停止，保留剩余 budget 做有用的工作。
**Source evidence**: Claude Code 内部的 compact 重试逻辑。OMC 的 `hasAttemptedReactiveCompact` flag 防止重复触发。

### E3.3: Post-Compact File Restoration
**What it does**: Compact 完成后，自动重新读取 agent 最近频繁访问的 top 5 文件，将内容重新注入 context。总恢复预算限制在 50K token 以内。文件按"最近 10 轮内的访问频次"排序。
**Why it matters**: Compact 摘要无法保留代码文件的精确内容——行号、变量名、函数签名在摘要中丢失。但 agent 下一步工作大概率还在操作这些文件。Pre-emptive restoration 让 agent 在 compact 后不用手动重新 Read 文件，减少一轮不产出的"恢复轮次"。
**Source evidence**: OMC 的 post-compact restoration 逻辑。Claude Code 的 compact 后文件重加载行为。

### E3.4: Layered Instruction Loading
**What it does**: 按优先级从高到低加载 4 层指令：(1) managed instructions（Claude Code 内置的系统指令）；(2) user instructions（`~/.claude/CLAUDE.md`）；(3) project instructions（项目根的 `CLAUDE.md`）；(4) local instructions（子目录的 `CLAUDE.md`）。后加载的指令可以 override 前面的，但不能撤销 managed instructions。
**Why it matters**: 不同粒度的指令解决不同问题——系统安全规则不应该被项目配置覆盖，但项目编码规范应该能覆盖全局偏好。分层加载 + 优先级确保"越局部越优先"的同时保护系统级约束。
**Source evidence**: Claude Code 的 CLAUDE.md 加载链。`--add-dir` 多目录支持。managed instructions 的不可覆盖性。

### E3.5: @include with Extension Whitelist
**What it does**: CLAUDE.md 中的 `@path/to/file` 语法允许包含外部文件的内容，但只允许特定扩展名（`.md`, `.txt`, `.yaml`, `.json` 等）。可执行文件、二进制文件被拒绝包含。
**Why it matters**: `@include` 是强大的模块化工具——把长指令拆到多个文件里。但如果不限制文件类型，`@/path/to/binary` 会把二进制垃圾注入 context，浪费 token 甚至导致解析错误。Extension whitelist 是最小权限原则在 include 机制上的体现。
**Source evidence**: Claude Code 的 `@` 文件引用语法实现。CLAUDE.md include 的安全限制。

### E3.6: Dynamic Prompt Cache Boundary
**What it does**: 将 prompt 中不变的部分（系统指令、CLAUDE.md、工具定义）放在前面形成 prompt cache boundary，变化的部分（对话消息、工具结果）放在后面。Anthropic API 的 prompt caching 对前缀相同的请求复用 KV cache，大幅降低 TTFT（首 token 延迟）和成本。
**Why it matters**: Agent 的每轮 API 调用有大量重复前缀（系统指令 + 工具定义可达 20-40K token）。没有 prompt cache 时每轮都要重新处理这些前缀，浪费计算和时间。动态调整 cache boundary（在指令变化时移动 boundary）让 cache hit rate 最大化。
**Source evidence**: Anthropic prompt caching 文档。Claude Code 的请求构造逻辑中的 cache_control breakpoint 放置策略。


path: references/filesystem-working-memory.md
content: # Filesystem Working Memory

## Problem

Agent 在长会话中积累的决策、计划、中间结论只存在于 context window 中。一旦 compact 或 session 结束，这些信息丢失。下次 compact 后 agent 不记得"为什么选了方案 B 而不是方案 A"，导致重复决策或矛盾操作。

## Solution

用 `.working-state/` 目录作为 agent 的"外部工作记忆"。包含两个核心文件：`current-plan.md`（当前执行计划，agent 随时读写）和 `decisions.jsonl`（决策日志，append-only）。Agent 在做出关键决策时写入 decisions.jsonl，在更新计划时覆写 current-plan.md。Compact 后 agent 通过读取这些文件恢复状态。

## Implementation

1. 任务启动时创建 `.working-state/` 目录

```bash
mkdir -p .working-state
```

2. `current-plan.md`：Agent 的当前执行计划

```markdown
# 当前计划（更新于 iteration 12）

## 已完成
- [x] 分析 parser.ts 结构
- [x] 添加新 AST 节点类型

## 进行中
- [ ] 更新 visitor pattern

## 待做
- [ ] 写测试
- [ ] 更新文档
```

3. `decisions.jsonl`：每行一个决策记录

```jsonl
{"ts":"2026-04-06T10:00:00Z","decision":"选择 visitor pattern 而非 switch-case","reason":"可扩展性，未来新增节点类型不需要改核心逻辑","alternatives":["switch-case (更简单但不可扩展)"]}
{"ts":"2026-04-06T10:15:00Z","decision":"parser 错误用 Result 类型而非 throw","reason":"调用方可以 pattern match，不会遗漏错误处理"}
```

4. Post-compact 恢复：在 CLAUDE.md 或 system prompt 中指示 agent：

```
每次 compact 后，先读取 .working-state/current-plan.md 和 .working-state/decisions.jsonl 恢复上下文。
```

5. 可选的辅助文件：
   - `scratch.md`：临时笔记，不保证存活
   - `blocked.md`：当前阻塞项和原因

## Tradeoffs

- **Pro**: 信息在 compact、crash、session 切换中存活
- **Pro**: 决策日志让 agent（和人类）理解历史推理过程
- **Con**: Agent 需要"记得"写入这些文件——需要在 prompt 中明确要求
- **Con**: 文件读写消耗 context token（但远小于丢失信息后的重复探索）

## Source

Claude Code 的 CLAUDE.md persistent memory 机制的扩展。OMC 的 handoff 文档系统。Last Whisper Dev 的 "filesystem as memory" 实践。


path: references/handoff-documents.md
content: # Pattern 3.1: Handoff 文档（上下文存活）

## 问题

Claude Code 的 context window 有限。长任务中会触发 auto-compact（4 级压缩：MicroCompact → Session Memory → Full Compact → Reactive Compact），压缩后关键设计决策、排除过的方案、已识别的风险都会丢失。

## 原理

在阶段结束或 context 压缩前，将关键信息写入磁盘文件。压缩后的 agent 可以通过读取这些文件恢复上下文。这比依赖 Claude Code 的压缩摘要更可控——你决定保留什么，而不是让压缩算法决定。

来自 OMC 的 `.omc/handoffs/<stage-name>.md` 机制——每个 team pipeline 阶段产出一份 handoff 文档，包含 Decided/Rejected/Risks/Files/Remaining 五段。

## 文档结构（5 个必要段落）

```markdown
# Handoff: <stage-name>

## Decided
- 选择 Redis 作为缓存方案，因为项目已有 Redis 依赖，无需新增基础设施
- 使用 LRU 策略，TTL 设为 5 分钟

## Rejected
- 排除 Memcached：团队无运维经验
- 排除本地文件缓存：不支持多实例部署

## Risks
- Redis 单点故障需要 Sentinel（当前未配置）
- 缓存穿透风险：高频查询的 key 需要布隆过滤器

## Files Modified
- src/cache/redis_client.py — 新建，Redis 连接池封装
- src/api/handlers.py:45-67 — 添加缓存查询层
- tests/test_cache.py — 缓存命中/未命中/过期测试

## Remaining
- Sentinel 配置（下个迭代）
- 缓存预热逻辑
- 监控指标接入
```

## 存储位置

```
sessions/<session-id>/handoffs/
  stage-1-plan.md
  stage-2-implement.md
  stage-3-verify.md
  pre-compact.md          ← 压缩前自动抢救（见 Pattern 3.2）
```

## 注入方式

在 agent 的 prompt 尾部追加：

```
在完成当前阶段前，将关键决策写入 handoff 文档：
sessions/<session-id>/handoffs/stage-<当前阶段>.md
必须包含 5 个段落：Decided / Rejected / Risks / Files Modified / Remaining
```

下一阶段的 agent 启动时，通过 UserPromptSubmit hook 或 prompt 注入最新的 handoff 文档。配合 `once: true` 标记确保只注入一次（见 Pattern 2.5）。

## 为什么不依赖 Claude Code 的内置压缩

Claude Code 的 Full Compact 使用 LLM 生成 9 段式结构化摘要，质量不错但有两个问题：

1. **摘要内容由 LLM 决定**，你无法控制保留什么
2. **使用 `<analysis>` scratchpad 提高摘要质量但 strip 后注入**（chain-of-thought 不进入压缩后的 context），意味着推理过程丢失

Handoff 文档让你显式控制保留的信息，和内置压缩互补而非替代。

## 已知局限

- Handoff 文档可能跨阶段产生矛盾（stage-2 推翻了 stage-1 的决策但没更新 stage-1 的 handoff）
- 过多阶段后，累积的 handoff 内容本身成为 context 负担
- Agent 可能不遵守写 handoff 的指令——这是概率性的，不是系统保证

用 Pattern 3.3（记忆合并）可以部分缓解 staleness 和碎片化问题。

## Claude Code 的 4 级压缩（背景知识）

> 以下信息基于 Claude Code 源码分析（v2.1.88），内部实现可能随版本变化。

| 级别 | 方法 | 压缩率 | 成本 |
|------|------|--------|------|
| MicroCompact | 手术式移除旧 tool results（8 种特定工具类型） | 10-50K tokens | 零（无 LLM 调用） |
| Session Memory | 用预建的背景摘要替换旧消息 | 60-80% | 零（摘要由后台 agent 维护） |
| Full Compact | LLM 生成 9 段结构化摘要 | 80-95% | 一次 API 调用 + 全缓存失效 |
| Reactive Compact | API 返回 413 后的紧急压缩 | 可变 | 应急措施 |

Handoff 文档在任何级别的压缩中都不会丢失——因为它在磁盘上，不在 context 里。


path: references/memory-consolidation.md
content: # Pattern 3.3: 三门控记忆合并（3-Gate Consolidation）

## 问题

跨多个 session 积累了大量碎片化的 handoff 文档和记忆文件，重复信息多、互相矛盾。

## 原理

定期合并记忆，但用三个门控避免不必要的合并操作。来自 Claude Code 内部的 AutoDream——一个 "sleep consolidation" 机制，在 session 之间自动整理积累的知识。

## 三个门控

1. **Time Gate**：距上次合并 >= 24 小时。避免频繁合并。
2. **Session Gate**：有 >= 5 个新 session 积累。确保有足够的新内容值得合并。
3. **Lock Gate**：获取文件锁确认无其他进程正在合并。10 次重试，5-100ms 指数退避。

三个门控按计算成本从低到高排列。时间检查最便宜（读一个时间戳），session 计数次之（数文件数），加锁最贵（文件系统操作）。任一门控失败，跳过本次合并。

## 合并操作

通过门控后，遍历所有 `sessions/*/handoffs/` 目录：
1. 按时间排序所有 handoff 文档
2. 提取 Decided/Rejected 段落
3. 合并去重，解决冲突（后来的决策覆盖早期的）
4. 写入精简的经验总结文件

## Claude Code 的 AutoDream 机制

AutoDream 在 session 之间运行，通过 `runForkedAgent` 生成一个独立 agent：
- 读取积累的 transcripts
- 编辑 memdir 中的 Markdown 文件来 consolidate learnings
- 通过 `TaskListV2` 提供实时状态更新

Memory taxonomy：
- `user`：个人偏好和习惯
- `feedback`：纠正和表扬
- `project`：架构、非显而易见的目标
- `reference`：外部文档和流程

安全措施包括 `sanitizePathKey`（拒绝 null bytes、URL 编码遍历、Unicode 规范化攻击）和 `realpathDeepestExisting`（解析 symlinks 防止逃逸）。


path: references/token-budget.md
content: # Pattern 3.4: Token Budget Per Subtask（子任务 token 预算）

## 问题

Agent 在处理复杂任务时，前半部分消耗了大量 context（读了太多文件），后半部分 context 不够用，被迫压缩丢失关键信息。没有机制在执行前预估和控制每个子任务的 context 消耗。

来源：Claude Code 官方文档 Context Window + Best Practices

## 原理

在 UserPromptSubmit hook 中估算当前 context 使用量，注入预算感知指令。当 context 接近阈值时，提醒 agent 使用 subagent 委托（subagent 有独立 context window）或触发主动压缩。

## Claude Code 官方建议

> "Claude's context window fills up fast, and performance degrades as it fills." 这是需要管理的最重要资源。

三个官方工具：
- `/context`：实时查看各类别的 context 占用
- `/compact`：手动触发压缩，用结构化摘要替换会话历史
- Subagent：每个有独立 context window，结果以摘要形式返回

## 实现

### UserPromptSubmit hook

```bash
# 从 hook 输入获取 transcript 路径（UserPromptSubmit hook 提供 transcript_path）
INPUT=$(cat)
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // ""')
if [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then
  # context-usage.sh 输出 "Input tokens: NNNNN"（原始 token 数，非百分比）
  # 因为 Claude Code 不在 transcript 中暴露 context_window_size
  INPUT_TOKENS=$(bash context-usage.sh "$TRANSCRIPT" | grep -o '[0-9]*$')
  if [ -n "$INPUT_TOKENS" ]; then
    # 粗略阈值：200K context window 的 80% ≈ 160K tokens
    if [ "$INPUT_TOKENS" -ge 160000 ]; then
      jq -n --arg ctx "WARNING: Input tokens at ${INPUT_TOKENS}. Context is heavily loaded. For remaining work: 1) Use subagents for file-heavy tasks 2) Avoid reading entire files, use targeted grep 3) Consider /compact if quality is degrading." \
        '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":$ctx}}'
    elif [ "$INPUT_TOKENS" -ge 120000 ]; then
      jq -n --arg ctx "Input tokens at ${INPUT_TOKENS}. Budget remaining work carefully — prefer grep over full file reads." \
        '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":$ctx}}'
    fi
  fi
fi
```

## 预算分配策略

| Context 使用率 | 策略 |
|---------------|------|
| < 40% | 自由读取，无限制 |
| 40-60% | 提醒 agent 注意读取量 |
| 60-80% | 注入"优先用 grep/subagent"指令 |
| 80-95% | 注入"必须用 subagent 委托，不要直接读大文件" |
| >= 95% | Ralph MUST 放行 stop（Pattern 1.1 安全阀） |

## 与 Pattern 3.5（Context 估算）的关系

Pattern 3.5 提供了底层的 context 使用量检测能力。Pattern 3.4 在此基础上做决策和干预——不只是"知道用了多少"，而是"根据用量调整行为"。

## Tradeoff

- 估算不精确——bytes/4 是粗估，实际 token 数可能偏差 20-30%
- 过早限制可能降低工作质量——agent 被迫用 grep 而不是读全文，可能遗漏上下文
- Subagent 委托引入延迟——每次委托需要额外的 API 调用


path: scripts/compaction-extract.sh
content: #!/usr/bin/env bash
# compaction-extract.sh — Stop hook: extract decisions to handoff file periodically
# Writes last assistant context to handoff document every N stops.
# Since Claude Code doesn't expose context_window_size to hooks, this fires
# periodically as a precaution rather than at a threshold.

set -euo pipefail

SESSIONS_DIR="${HOME}/.openclaw/shared-context/sessions"
EXTRACT_INTERVAL="${COMPACTION_EXTRACT_INTERVAL:-15}"  # Every 15 stops

INPUT=$(head -c 20000)
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null)
[ -z "$SESSION_ID" ] && SESSION_ID="${NC_SESSION:-}"
[ -z "$SESSION_ID" ] && echo '{"continue":true}' && exit 0

SESSION_DIR="${SESSIONS_DIR}/${SESSION_ID}"
HANDOFFS_DIR="${SESSION_DIR}/handoffs"
STATE_FILE="${SESSION_DIR}/compaction-extract.json"
mkdir -p "$HANDOFFS_DIR"

# Track stop count
STOP_COUNT=0
if [ -f "$STATE_FILE" ]; then
  STOP_COUNT=$(jq -r '.stop_count // 0' "$STATE_FILE" 2>/dev/null)
fi
STOP_COUNT=$((STOP_COUNT + 1))

# Update state
TMP="${STATE_FILE}.${$}.tmp"
jq -n --argjson count "$STOP_COUNT" '{"stop_count":$count}' > "$TMP"
mv "$TMP" "$STATE_FILE"

# Only extract every N stops
if [ $((STOP_COUNT % EXTRACT_INTERVAL)) -ne 0 ]; then
  echo '{"continue":true}'
  exit 0
fi

# Extract last assistant message as knowledge to preserve
LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""' 2>/dev/null | head -c 5000)
[ -z "$LAST_MSG" ] && echo '{"continue":true}' && exit 0

TIMESTAMP=$(date +%s)
HANDOFF_FILE="${HANDOFFS_DIR}/pre-compact-${TIMESTAMP}.md"

cat > "$HANDOFF_FILE" << EOF
# Pre-Compaction Knowledge Extract
Timestamp: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
Session: ${SESSION_ID}
Stop count: ${STOP_COUNT}

## Context Summary
${LAST_MSG}
EOF

jq -n --arg ctx "Knowledge snapshot saved to handoffs/pre-compact-${TIMESTAMP}.md (stop #${STOP_COUNT})." \
  '{"hookSpecificOutput":{"additionalContext":$ctx}}'


path: scripts/context-usage.sh
content: #!/usr/bin/env bash
# context-usage.sh — Extract input token count from Claude Code transcript
# Usage: context-usage.sh <transcript-jsonl-path>
#
# IMPORTANT: Claude Code transcripts do NOT contain a context_window_size
# field. Context window data is only available via the statusLine stdin pipe
# (used by HUD plugins). This script can only report raw input_tokens from
# the last API response — it CANNOT compute a usage percentage.
#
# Output: "Input tokens: NNNNN" or nothing if unavailable

set -euo pipefail

TRANSCRIPT="${1:-}"
[ -z "$TRANSCRIPT" ] && exit 0
[ -f "$TRANSCRIPT" ] || exit 0

SIZE=$(stat -f%z "$TRANSCRIPT" 2>/dev/null || stat -c%s "$TRANSCRIPT" 2>/dev/null || echo 0)
[ "$SIZE" -lt 100 ] && exit 0

# Get the last complete JSONL line (use tail -1, not tail -c 4096,
# because real JSONL lines can be 20KB+)
LAST_LINE=$(tail -1 "$TRANSCRIPT" 2>/dev/null)
[ -z "$LAST_LINE" ] && exit 0

# Extract input_tokens from nested usage object (real format: "usage":{"input_tokens":N,...})
INPUT_TOKENS=$(echo "$LAST_LINE" | jq -r '.usage.input_tokens // empty' 2>/dev/null || true)

# Filter out streaming placeholders (input_tokens <= 10 is likely a stream marker)
if [ -n "$INPUT_TOKENS" ] && [ "$INPUT_TOKENS" -gt 10 ] 2>/dev/null; then
  echo "Input tokens: ${INPUT_TOKENS}"
fi


path: tests/test_compaction_extract.py
content: """Tests for compaction-extract stop hook."""

import json
import os
import subprocess
from pathlib import Path

import pytest

SCRIPTS_DIR = Path(__file__).parent.parent / "scripts"


@pytest.fixture
def env(tmp_path):
    e = os.environ.copy()
    e["HOME"] = str(tmp_path)
    sessions = tmp_path / ".openclaw" / "shared-context" / "sessions"
    sessions.mkdir(parents=True)
    return e, tmp_path


def run(env, stdin_data="", session_id="test-sess"):
    env["NC_SESSION"] = session_id
    result = subprocess.run(
        ["bash", str(SCRIPTS_DIR / "compaction-extract.sh")],
        capture_output=True, text=True, env=env, input=stdin_data, timeout=10,
    )
    return result.stdout.strip(), result.returncode


class TestCompactionExtract:
    def test_first_call_allows_and_counts(self, env):
        e, tmp = env
        inp = json.dumps({"last_assistant_message": "working on feature"})
        stdout, rc = run(e, inp)
        assert rc == 0
        result = json.loads(stdout)
        assert result.get("continue") is True
        # State file should have count 1
        state = tmp / ".openclaw/shared-context/sessions/test-sess/compaction-extract.json"
        assert state.exists()
        data = json.loads(state.read_text())
        assert data["stop_count"] == 1

    def test_count_below_interval_allows(self, env):
        e, tmp = env
        e["COMPACTION_EXTRACT_INTERVAL"] = "5"
        inp = json.dumps({"last_assistant_message": "doing stuff"})
        for _ in range(4):
            stdout, rc = run(e, inp)
            result = json.loads(stdout)
            assert result.get("continue") is True

    def test_at_interval_extracts(self, env):
        e, tmp = env
        e["COMPACTION_EXTRACT_INTERVAL"] = "3"
        inp = json.dumps({"last_assistant_message": "Important decision: use Redis for caching."})
        # Calls 1 and 2 should allow
        run(e, inp)
        run(e, inp)
        # Call 3 should extract
        stdout, rc = run(e, inp)
        assert rc == 0
        result = json.loads(stdout)
        ctx = result.get("hookSpecificOutput", {}).get("additionalContext", "")
        assert "snapshot saved" in ctx
        assert "stop #3" in ctx
        # Handoff file should exist
        handoffs = tmp / ".openclaw/shared-context/sessions/test-sess/handoffs"
        files = list(handoffs.glob("pre-compact-*.md"))
        assert len(files) == 1
        content = files[0].read_text()
        assert "Redis for caching" in content

    def test_no_session_allows(self, env):
        e, _ = env
        e["NC_SESSION"] = ""
        inp = json.dumps({"last_assistant_message": "test"})
        stdout, rc = run(e, inp)
        assert rc == 0
        result = json.loads(stdout)
        assert result.get("continue") is True

    def test_empty_message_at_interval_allows(self, env):
        e, tmp = env
        e["COMPACTION_EXTRACT_INTERVAL"] = "2"
        # Empty message — even at interval, should not extract
        inp = json.dumps({"last_assistant_message": ""})
        run(e, inp)
        stdout, rc = run(e, inp)  # 2nd call, at interval
        result = json.loads(stdout)
        # Should allow (no content to extract)
        assert result.get("continue") is True


path: tests/test_context_usage.py
content: """Tests for context-usage.sh script."""

import json
import os
import subprocess
import tempfile
from pathlib import Path

import pytest

SCRIPTS_DIR = Path(__file__).parent.parent / "scripts"


def run_context_usage(transcript_content):
    with tempfile.NamedTemporaryFile(mode='w', suffix='.jsonl', delete=False) as f:
        f.write(transcript_content)
        f.flush()
        result = subprocess.run(
            ["bash", str(SCRIPTS_DIR / "context-usage.sh"), f.name],
            capture_output=True, text=True, timeout=10,
        )
        os.unlink(f.name)
        return result.stdout.strip()


class TestContextUsage:
    def test_extracts_tokens_from_realistic_format(self):
        """Real Claude Code transcripts have usage nested in a large JSON object."""
        # Simulate a realistic final JSONL line with nested usage
        line = json.dumps({
            "type": "assistant",
            "message": {"role": "assistant", "content": "Done."},
            "usage": {
                "input_tokens": 150000,
                "cache_creation_input_tokens": 0,
                "cache_read_input_tokens": 120000,
                "output_tokens": 500,
            }
        })
        output = run_context_usage(line + "\n")
        assert "Input tokens: 150000" in output

    def test_filters_streaming_placeholder(self):
        """Streaming entries have input_tokens=1, should be filtered."""
        line = json.dumps({"type": "stream", "usage": {"input_tokens": 1, "output_tokens": 0}})
        output = run_context_usage(line + "\n")
        assert output == ""

    def test_empty_file_no_output(self):
        output = run_context_usage("")
        assert output == ""

    def test_no_usage_field(self):
        line = json.dumps({"type": "message", "content": "hello"})
        output = run_context_usage(line + "\n")
        assert output == ""

    def test_uses_last_line(self):
        """Should read the last line, not intermediate lines."""
        lines = []
        for i in range(50):
            lines.append(json.dumps({"type": "stream", "usage": {"input_tokens": 1, "output_tokens": 0}}))
        # Last line has the real count
        lines.append(json.dumps({"usage": {"input_tokens": 200000, "output_tokens": 1000}}))
        output = run_context_usage("\n".join(lines) + "\n")
        assert "Input tokens: 200000" in output


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)