mcpField
D
dream

dream

github.com/yonatangrossAgent 工作流30075 次安装今天更新
代码仓库

关于 dream

用于确定性维护 Agent 记忆文件的技能。它按可验证规则检查过期引用、重复项、矛盾和索引一致性,并以保守保护措施避免依赖大模型判断删除内容。

主要功能

  1. 01检查过期文件和符号引用
  2. 02检测重复记忆条目
  3. 03标记相互矛盾的记忆
  4. 04重建并校验记忆索引
  5. 05预览更改并谨慎清理

适用场景

  1. 01清理累积的会话记忆
  2. 02查找过期的项目引用
  3. 03整理重复或冲突的笔记
  4. 04在修改前预览记忆清理结果

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/yonatangross/orchestkit --skill dream

安装后在 Claude Code 中输入/dream确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
housekeeping.md
index-budget.md
safe-deletes.md
scripts/
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:检测重复项

在同一目录中逐对比较记忆。满足以下条件时,两条记忆视为重复项:

  1. 类型相同(两者都是 feedback、project 等)
  2. 主题重叠——两者正文中有 60% 或以上的关键词相同(不包括停用词)
  3. 主体相同——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:解决矛盾

当两条相同类型的记忆针对同一主体提出相反观点时,就存在矛盾。检测方式:

  1. 类型相同且主题相同(重叠率 >= 0.4 且 < 0.6——相关但不重复)
  2. 否定信号——一条记忆的正文否定了另一条记忆的断言:
    • “执行 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——搜索、加载、同步、可视化(读取端)

评论

支持 Markdown