关于 dream
用于确定性维护 Agent 记忆文件的技能。它按可验证规则检查过期引用、重复项、矛盾和索引一致性,并以保守保护措施避免依赖大模型判断删除内容。
主要功能
- 01检查过期文件和符号引用
- 02检测重复记忆条目
- 03标记相互矛盾的记忆
- 04重建并校验记忆索引
- 05预览更改并谨慎清理
适用场景
- 01清理累积的会话记忆
- 02查找过期的项目引用
- 03整理重复或冲突的笔记
- 04在修改前预览记忆清理结果
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ yonatangross/ orchestkit --skill dream安装后在 Claude Code 中输入/dream确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/housekeeping.mdindex-budget.mdsafe-deletes.mdscripts/index-budget.mjs详细介绍
Dream - 记忆整合
确定性的记忆维护:检测过时条目、合并重复内容、解决矛盾,并重建 MEMORY.md 索引。所有清理决策都基于可验证的检查(文件是否存在?函数是否存在?内容是否重复?),而非大模型的判断。
参数解析
DRY_RUN = "--dry-run" in "$ARGUMENTS" # Preview changes without writing
概述
记忆文件会在多个会话中不断累积。随着时间推移,它们会出现以下问题:
- 过时引用 — 指向已不存在的文件、函数或类的记忆
- 重复内容 — 多条记忆涉及相同主题,内容有所重叠
- 矛盾 — 新记忆取代旧记忆,却未清理旧内容
- 索引偏移 — MEMORY.md 索引与实际记忆文件不同步
此技能仅使用确定性检查来解决以上四个问题。
完成标准。 完成意味着:根据保留的文件重建 MEMORY.md 并通过 STEP 5.5 验证,打印 STEP 6 报告,且 STEPs 7 到 9 均已运行,或打印了一行跳过原因。
预算:对发现的记忆文件进行一次读取(STEP 1),重写一次 MEMORY.md(STEP 5),若索引超出预算,则重写一次 STEP 5.5 末尾部分;不使用子 Agent(所有检查均为确定性的);到达完成标准或首个上限时停止并报告,以先发生者为准。
运行频率(CC 2.1.142+): 响应式压缩现在会根据实际溢出量调整首次摘要尝试的规模,因此长会话在轮次中途卡住的情况会大幅减少。“每晚运行”的频率可以放宽为“记忆文件累积时运行”——现在无需再通过整合来避免压缩效率低下。
STEP 1:发现记忆文件
# Find the memory directory (agent-specific or project-level)
# Agent memory lives in: .claude/agent-memory/<agent-id>/
# Project memory lives in: .claude/projects/<hash>/memory/
# Also check: .claude/memory/
memory_dirs = []
Glob(pattern=".claude/agent-memory/*/MEMORY.md")
Glob(pattern=".claude/projects/*/memory/MEMORY.md")
Glob(pattern=".claude/memory/MEMORY.md")
# For each discovered MEMORY.md, glob all *.md files in that directory
for dir in memory_dirs:
Glob(pattern=f"{dir}/../*.md") # All memory files alongside MEMORY.md
读取每个发现的记忆文件。解析前置元数据(name、description、type)和正文内容。构建内存中的清单:
inventory = [{
"path": "/abs/path/to/file.md",
"name": frontmatter.name,
"type": frontmatter.type, # user, feedback, project, reference
"description": frontmatter.description,
"body": body_text,
"file_refs": [], # extracted file paths
"symbol_refs": [], # extracted function/class names
"topics": [], # key phrases for duplicate detection
}]
STEP 2:检测过时内容
对每个记忆文件提取引用,并验证它们是否仍然存在。
2a:文件路径引用
提取看起来像文件引用的路径(模式:包含 / 和文件扩展名的路径、用反引号括起来的路径):
# Regex-like extraction from body text:
# - Paths containing / with common extensions: .py, .ts, .tsx, .js, .json, .md, .yaml, .yml, .sh
# - Backtick-wrapped paths: `src/something/file.ts`
# - Quoted paths in frontmatter descriptions
验证之前,先判定每个引用的范围。 Glob 只能搜索当前仓库,因此位于其他位置的路径永远不会匹配,否则就会被判为缺失。关于 ~/.claude hooks、homebrew cask、cmux 配置或其他仓库的记忆,不会仅仅因为当前仓库不包含这些内容就过时。
def scope(ref):
# Anything rooted outside the working repo is UNVERIFIABLE, not missing.
if ref.startswith(("~", "/", "$")): return "UNVERIFIABLE"
if ref.startswith(("http://", "https://")): return "UNVERIFIABLE"
if re.match(r'^[A-Za-z0-9_.-]+/', ref) and not (REPO / ref.split("/")[0]).exists():
return "UNVERIFIABLE" # first segment is not a real top-level dir here
return "REPO_RELATIVE"
verifiable = [r for r in file_refs if scope(r) == "REPO_RELATIVE"]
external = [r for r in file_refs if scope(r) == "UNVERIFIABLE"]
missing = []
for ref in verifiable:
Glob(pattern=ref)
# If no match → missing.append(ref)
过时率仅基于 verifiable 引用计算。 external 引用会记录在报告中,但绝不会计入清理判断。没有 verifiable 引用的记忆,无论提及多少外部路径,都属于 EVERGREEN。
2b:符号引用
提取函数/类名称(模式:function_name()、ClassName、def function_name):
for symbol in symbol_refs:
Grep(pattern=symbol, path=".", output_mode="files_with_matches", head_limit=1)
# If no match → mark as STALE_SYMBOL_REF
2c:过时分类
| 发现 | 分类 | 操作 |
|---|---|---|
| VERIFIABLE 引用为零(没有引用,或全部均为 UNVERIFIABLE) | EVERGREEN | 保留 |
| 所有 verifiable 引用均有效,且找到了所有符号 | FRESH | 保留 |
| 部分 verifiable 引用缺失 | PARTIALLY_STALE | 标记以供审核 |
| 所有 verifiable 引用均缺失,且所有符号也均缺失 | FULLY_STALE | 候选清理项 |
只有被分类为 FULLY_STALE 的记忆会自动清理。PARTIALLY_STALE 记忆会报告但保留——由用户决定。
2d:清理保护条件——分类完成后、删除前检查
FULLY_STALE 是删除的必要条件,但并非充分条件。以下每项保护条件都会将分类降级为 PARTIALLY_STALE(保留并标记)。这些保护条件存在的原因是:记忆文件不在 git 中,错误删除不会留下痕迹且无法恢复,因此在不确定时始终优先保留。
GUARD_DAYS = 14
for m in list(fully_stale_files):
reason = None
# 1. Preferences do not decay because a path moved.
if m["type"] == "user":
reason = "type:user is never auto-pruned"
# 2. A feedback/reference memory carries a LESSON; the file paths in it are
# illustrations, not a manifest. Its worth does not expire when an
# illustrative path moves, and ref-extraction is lossy anyway (it catches
# `file.ts` but misses `file.ts:186` and `functionName()`). Only project
# memories — which track live work against concrete files — are eligible
# to go fully stale on ref death.
elif m["type"] in ("feedback", "reference"):
reason = f"type:{m['type']} value is the lesson, not its file refs"
# 3. Recently written memories describe the present, whatever their refs say.
elif (now - m["mtime"]) < GUARD_DAYS * 86400:
reason = f"modified within {GUARD_DAYS}d"
# 4. A memory that exists to prevent a regression must outlive the code it cites.
elif re.search(r'\b(do not|don\'t|never|avoid)\b', m["body"], re.I):
reason = "carries a do-not/never directive"
if reason:
m["classification"] = "PARTIALLY_STALE"
m["kept_reason"] = f"prune-guard: {reason}"
fully_stale_files.remove(m)
partially_stale_files.append(m)
第 3 条保护条件最为微妙。reference_cmux_scroll_blank_research 写道 “已解决;不要在 cmux 上再次建议 tui:fullscreen”,而它引用的所有路径都已迁移。删除它会重新引入它原本要防止的回归问题。对于一条价值在于禁止某种做法的记忆来说,原始代码消失之时,恰恰是它最有用的时候。
步骤 2.5:咨询门控(#2351)——绝不清理仍在使用的记忆
闭合 VERIFY 循环:删除操作必须经得起“是否真的查询过这条记忆?”这一问题。某条记忆的所有外部引用都已消失(FULLY_STALE),但 Agent 仍不断查找它,那么它仍然承载着重要信息——它的引用已过时,它的知识仍然有效。因此,清理前先读取 .claude/logs/memory-consult.jsonl(memory-validator 会在每次 mcp__memory__search_nodes/open_nodes/read_graph 时写入),并将近期查询过的所有 FULLY_STALE 记忆降级为 PARTIALLY_STALE(予以保留并标记,而非自动删除)。
import json, time
from pathlib import Path
def recently_consulted_terms(days=14):
log = Path(".claude/logs/memory-consult.jsonl")
if not log.exists():
return set()
cutoff = time.time() - days * 86400
terms = set()
for line in log.read_text().splitlines():
try:
e = json.loads(line)
except ValueError:
continue # best-effort: skip malformed lines
# open_nodes carries exact entity names; search carries a query string
terms.update(n.lower() for n in e.get("names", []))
if e.get("query"):
terms.update(w.lower() for w in e["query"].split() if len(w) > 2)
return terms
consulted = recently_consulted_terms()
for m in list(fully_stale_files):
slug = Path(m["path"]).stem.lower()
name = (m.get("name") or "").lower()
if name in consulted or any(c in slug or slug in c for c in consulted):
m["classification"] = "PARTIALLY_STALE"
m["kept_reason"] = "consult-gate: looked up in the last 14 days (#2351)"
fully_stale_files.remove(m)
partially_stale_files.append(m)
这是有意采取的保守设计——模糊词语匹配宁可保留可能被查询过的记忆,也不删除仍有效的记忆。步骤 6 的报告会记录每条因咨询门控而保留的记忆(补上此前循环中缺失的“这条记忆是否有用?”审计)。如果日志不存在(尚未运行咨询记录机制),门控将不执行任何操作,清理照常进行。
步骤 3:检测重复项
在同一目录中逐对比较记忆。满足以下条件时,两条记忆视为重复项:
- 类型相同(两者都是
feedback、project等) - 主题重叠——两者正文中有 60% 或以上的关键词相同(不包括停用词)
- 主体相同——
name或description字段指向同一概念
stopwords = {"the", "a", "an", "is", "are", "was", "were", "be", "been",
"have", "has", "had", "do", "does", "did", "will", "would",
"could", "should", "may", "might", "can", "shall", "to", "of",
"in", "for", "on", "with", "at", "by", "from", "as", "into",
"through", "during", "before", "after", "this", "that", "it",
"not", "no", "but", "or", "and", "if", "then", "than", "so"}
def significant_words(text):
words = set(text.lower().split()) - stopwords
return {w for w in words if len(w) > 2}
def overlap_ratio(words_a, words_b):
if not words_a or not words_b:
return 0.0
intersection = words_a & words_b
smaller = min(len(words_a), len(words_b))
return len(intersection) / smaller if smaller > 0 else 0.0
# For each pair with same type:
# if overlap_ratio >= 0.6 → DUPLICATE pair
# Keep the NEWER file (by filesystem mtime), prune the older
步骤 4:解决矛盾
当两条相同类型的记忆针对同一主体提出相反观点时,就存在矛盾。检测方式:
- 类型相同且主题相同(重叠率 >= 0.4 且 < 0.6——相关但不重复)
- 否定信号——一条记忆的正文否定了另一条记忆的断言:
- “执行 X”与“不执行 X”/“别执行 X”/“永不执行 X”
- “使用 X”与“避免 X”/“停止使用 X”
- “偏好 X”与“偏好 Y”(针对相同决策领域)
negation_pairs = [
("do ", "do not "), ("do ", "don't "),
("use ", "avoid "), ("use ", "stop using "),
("prefer ", "don't prefer "), ("always ", "never "),
]
# For each pair flagged as contradictory:
# Keep the NEWER file (more recent decision supersedes)
# Prune the older file
步骤 5:执行更改(或试运行)
试运行模式(--dry-run)
如果存在 --dry-run 标志,则跳过所有写入操作。输出完整报告(步骤 6),并加上 [DRY RUN] 前缀,列出将要进行的更改:
[DRY RUN] Would delete: .claude/agent-memory/foo/stale_old_path.md (FULLY_STALE)
[DRY RUN] Would delete: .claude/agent-memory/foo/duplicate_auth.md (DUPLICATE of auth_patterns.md)
[DRY RUN] Would delete: .claude/agent-memory/foo/old_preference.md (CONTRADICTED by new_preference.md)
[DRY RUN] Would rebuild: .claude/agent-memory/foo/MEMORY.md (3 entries removed, 12 remaining)
实际执行模式
# Move, never rm: memory files are not in git (see references/safe-deletes.md)
trash = f"{memory_dir}/.trash/{date.today().isoformat()}"
Bash(command=f"mkdir -p '{trash}'")
# 1. FULLY_STALE files, 2. DUPLICATE (older), 3. CONTRADICTED (older)
to_remove = [s["path"] for s in fully_stale_files]
to_remove += [d["older"]["path"] for d in duplicate_pairs]
to_remove += [c["older"]["path"] for c in contradiction_pairs]
for path in to_remove:
Bash(command=f"mv '{path}' '{trash}/'")
# 4. Rebuild MEMORY.md index from surviving files (exclude .trash/ from the walk)
重建 MEMORY.md
读取所有保留的 .md 文件(不包括 MEMORY.md 本身)。生成索引:
# <Directory Name> Memory
- [Name](filename.md) -- one-line description from frontmatter
重建索引的规则:
- 每个记忆文件占一行,按文件名的字母顺序排列
- 限制因素是字节数,而不是行数。 每次会话都会加载 MEMORY.md,超过读取上限(约 24 KB)后便会停止加载,导致整个索引悄然失效。行数只是一个不够准确的替代指标:151 个条目,平均每行 147 个字符时,文件大小为 22.5 KB,几乎无法使用;同样 151 个条目,平均每行 112 个字符时,文件大小为 16.2 KB,则状态良好。
- 目标是总计不超过 17 KB。根据计算结果推导每行预算,而非将其硬编码:
budget_chars = (17 * 1024 - non_entry_overhead) / entry_count - 如果重建结果超过目标,应先将索引项精简至推导出的预算,再删除任何条目。在词语边界处截断,并保留开头的分句(其中包含用于区分条目的细节)。
- 一对一不变量适用于双文件结构(#3741)。 每个记忆文件都必须恰好在
MEMORY.md和MEMORY-ARCHIVE.md中被索引一次:indexed(MEMORY.md) + indexed(MEMORY-ARCHIVE.md) == files_on_disk,两组文件互不重叠,每个链接目标都必须以大小写完全一致的名称存在,并且MEMORY.md必须且只能有一条指向MEMORY-ARCHIVE.md的尾注行(> N memory files ...)。从files_on_disk中排除MEMORY*.md、.MEMORY.md.prev、.trash/和_backup/。单文件检查(indexed == files_on_disk)会将所有已归档条目标记为缺失,因此只要存在归档文件,这种检查就是错误的。 - 只有在精简至约 90 个字符后仍然超出目标时,才应警告用户。绝不为了让索引符合要求而自动删除记忆;索引是指针表,缩短它属于格式问题,而非保留问题。
# Write the rebuilt MEMORY.md. Copy to .MEMORY.md.prev FIRST: this one write
# replaces every memory's pointer, so a bad index degrades sessions silently.
Write(path="<memory_dir>/MEMORY.md", content=rebuilt_index)
STEP 5.5:索引预算(先报告,再按规则降级)
一致性(与文件一一对应)和预算(会话能承受的字节数)是两个不同的问题;
重建只解决第一个问题。预算检查通过脚本完成,因此无论谁运行,报告都相同。默认只读;只有在用户接受这些移动后才使用 --apply(超过 3 项时分批处理):
Bash(command=f"node ${{CLAUDE_PLUGIN_ROOT}}/skills/dream/scripts/index-budget.mjs '{memory_dir}' --json")
# bytes vs ceiling (ORK_CONTEXT_FILE_BUDGET_BYTES, else 17,408 B), per-section sizes, long entries,
# the two-file invariant, and moves proposed BY RULE (oldest first). Add --apply to rotate
# .MEMORY.md.prev, move the lines to MEMORY-ARCHIVE.md, rewrite the trailer, re-verify (exit 1 on failure).
规则、绝不移动集合及候选项耗尽时的回退方案:Read("references/index-budget.md")。
STEP 6:报告
合并后输出摘要表:
## Dream Consolidation Report
| Metric | Count |
|--------|-------|
| Memory directories scanned | N |
| Total memory files scanned | N |
| Stale entries pruned | N |
| Duplicates merged | N |
| Contradictions resolved | N |
| Partially stale (kept, flagged) | N |
| Evergreen (no external refs) | N |
| Surviving memories | N |
| MEMORY.md indexes rebuilt | N |
| Promotion candidates (2+ repos, STEP 9) | N |
### Changes Made
| File | Action | Reason |
|------|--------|--------|
| `path/to/file.md` | DELETED | Fully stale: all referenced files removed |
| `path/to/old.md` | DELETED | Duplicate of `path/to/new.md` |
| `path/to/outdated.md` | DELETED | Contradicted by `path/to/current.md` |
### Flagged for Review (PARTIALLY_STALE)
| File | Missing References |
|------|-------------------|
| `path/to/file.md` | `src/old/path.ts` no longer exists |
如果使用 --dry-run,在整个报告前加上:
[DRY RUN] No files were modified. Run without --dry-run to apply changes.
错误处理
| 条件 | 响应 |
|---|---|
| 未找到记忆目录 | 报告 "No memory directories found" 并退出 |
| 目录中没有记忆文件 | 报告 "Directory empty, nothing to consolidate" |
| 所有记忆都是 FRESH | 报告 "All N memories are current, nothing to prune" |
| 重建和降级后 MEMORY.md 仍超过字节上限 | 警告用户,列出剩余候选项,绝不自动截断 |
| 文件删除失败 | 报告错误,并继续处理剩余文件 |
| 记忆文件没有 frontmatter | 视为 EVERGREEN(没有元数据,无法验证引用) |
STEPs 7-8:整理
STEP 6 之后:读取 Read("references/housekeeping.md"),以执行 STEP 7(提供清理孤立插件的选项)和 STEP 8(提示项目状态过时,仅预览,绝不清除)。
STEP 9:跨仓库推广候选项 (#3295)
在 2+ 个项目中出现的记忆模式,说明其能力已超出原仓库的范围。 合并时,以确定性方式检测这些模式,并提供推广选项——dream 永远不会自行移动内容,因此即使此步骤由模型调用也能保持安全。
# For each memory file touched in this run, derive a topic key: the filename slug minus
# scope words (dates, project names). Then look for the same key in OTHER projects'
# memory indexes (index lines are "- [Title](file.md) -- hook"):
grep -l -i "<topic-key>" ~/.claude/projects/*/memory/MEMORY.md \
| grep -v "<current-project-dir>"
- 符合条件的不同项目达到 2 个或更多 -> 该记忆是推广候选项。
- 仅采用确定性匹配:依据规范化的 slug/标题词元进行匹配,绝不依赖语义判断。
- 误报代价很低(用户可以拒绝);漏报才是此步骤要避免的情况——同一条基础设施经验在 N 个仓库中反复重学,却无人察觉。
交互式运行: 对每个候选项询问 AskUserQuestion(超过 3 项时分批处理):
- “推广为共享插件”——组织专属模式放入组织的私有插件,通用模式放入公共插件;dream 只负责开启推广入口,由用户决定去向。
- “保留在本地”——确实只适用于特定仓库的重复模式。
- “不再建议此项”——将
promotion: declined追加到记忆的 frontmatter 元数据中,以便后续运行时跳过。
非交互式运行/试运行: 在 Dream Consolidation Report 的
Promotion candidates: 下列出候选项及匹配到的项目路径。不显示提示,不进行修改。
不适用场景
- 存储新决策时——使用
remember - 搜索以往决策时——使用
memory search - 在会话开始时加载上下文——使用
memory load - 会话少于 5 次时——记忆文件不太可能积累足够多的过时内容
相关技能
ork:remember——存储决策和模式(写入端)ork:memory——搜索、加载、同步、可视化(读取端)