---
title: "Dream"
description: "Nightly memory consolidation — prunes stale entries, merges duplicates, resolves contradictions, rebuilds MEMORY.md index. Use when memory files have accumulated over many sessions and need cleanup. Do NOT use for storing new decisions (use remember) or searching memory (use memory)."
canonical: "https://orchestkit.yonyon.ai/docs/reference/skills/dream"
---

# Dream

Nightly memory consolidation — prunes stale entries, merges duplicates, resolves contradictions, rebuilds MEMORY.md index. Use when memory files have accumulated over many sessions and need cleanup. Do NOT use for storing new decisions (use remember) or searching memory (use memory).

<span className="badge badge-blue">Command</span> <span className="badge badge-yellow">medium</span>

```bash title="Invoke"
/ork:dream
```

<ContextualSkillSidebar slug="dream" />

> **Dream** Nightly memory consolidation — prunes stale entries, merges duplicates, resolves contradictions, rebuilds MEMORY.md index. Use when memory files have accumulated over many sessions and need cleanup. Do NOT use for storing new decisions (use remember) or searching memory (use memory).


# Dream - Memory Consolidation

Deterministic memory maintenance: detect stale entries, merge duplicates, resolve contradictions, rebuild the MEMORY.md index. All pruning decisions are based on verifiable checks (file exists? function exists? duplicate content?), not LLM judgment.

## Argument Resolution

```python
DRY_RUN = "--dry-run" in "$ARGUMENTS"  # Preview changes without writing
```

## Overview

Memory files accumulate across sessions. Over time they develop problems:
- **Stale references** — memories pointing to files, functions, or classes that no longer exist
- **Duplicates** — multiple memories covering the same topic with overlapping content
- **Contradictions** — newer memories superseding older ones without cleanup
- **Index drift** — MEMORY.md index out of sync with actual memory files

This skill fixes all four problems using deterministic checks only.

> **Cadence (CC 2.1.142+):** Reactive compaction now sizes its first summarize attempt to the actual overflow, so long sessions stall mid-turn far less often. The "run nightly" cadence can relax toward "run when memory files accumulate" — consolidation is no longer needed to head off compaction inefficiency.

---

## STEP 1: Discover Memory Files

```python
# 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
```

Read every discovered memory file. Parse frontmatter (`name`, `description`, `type`) and body content. Build an in-memory inventory:

```
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: Detect Staleness

For each memory file, extract references and verify they still exist.

### 2a: File Path References

Extract paths that look like file references (patterns: paths with `/` and file extensions, backtick-wrapped paths):

```python
# 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
```

**Classify each ref's SCOPE before verifying it.** `Glob` only sees the current repo, so a path that
lives anywhere else can never match and would otherwise be scored as missing. A memory about
`~/.claude` hooks, a homebrew cask, a cmux config, or another repo is not stale just because this
repo does not contain it.

```python
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)
```

**The staleness ratio is computed over `verifiable` ONLY.** `external` refs are recorded for the
report and never counted toward pruning. A memory with zero verifiable refs is `EVERGREEN` no
matter how many external paths it names.

### 2b: Symbol References

Extract function/class names (patterns: `function_name()`, `ClassName`, `def function_name`):

```python
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: Staleness Classification

| Finding | Classification | Action |
|---------|---------------|--------|
| **Zero VERIFIABLE refs** (none, or all UNVERIFIABLE) | EVERGREEN | Keep |
| All verifiable refs valid, all symbols found | FRESH | Keep |
| Some verifiable refs missing | PARTIALLY_STALE | Flag for review |
| All verifiable refs missing AND all symbols missing | FULLY_STALE | Prune candidate |

Only memories classified as FULLY_STALE are auto-pruned. PARTIALLY_STALE memories are reported but kept — the user decides.

### 2d: Prune guards — checked AFTER classification, before any delete

`FULLY_STALE` is necessary but **not sufficient** to delete. Every guard below downgrades to
PARTIALLY_STALE (kept + flagged). These exist because memory files are **not in git**: a wrong
delete is silent and unrecoverable, so the asymmetry always favours keeping.

```python
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)
```

Guard 3 is the subtle one. `reference_cmux_scroll_blank_research` said *"RESOLVED; do NOT re-suggest
`tui:fullscreen` on cmux"* and every path it cited had moved. Deleting it reintroduces exactly the
regression it was written to prevent. A memory whose value is a prohibition is at its most useful
precisely when the original code is gone.

### STEP 2.5: Consult-gate (#2351) — never prune a memory that's still being used

Closing the VERIFY loop: a deletion must survive the question *"was this actually consulted?"*. A memory whose external refs all vanished (FULLY_STALE) but that the agent keeps looking up is still load-bearing — its **refs** are stale, its **knowledge** is live. So before pruning, read `.claude/logs/memory-consult.jsonl` (written by `memory-validator` on every `mcp__memory__search_nodes`/`open_nodes`/`read_graph`) and **downgrade any recently-consulted FULLY_STALE memory to PARTIALLY_STALE** (kept + flagged, not auto-deleted).

```python
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)
```

This is conservative by design — fuzzy term matching errs toward **keeping** a maybe-consulted memory rather than deleting a live one. The Step 6 report records each consult-gated keep (the "did it matter?" audit the loop was missing). If the log is absent (consult instrumentation not yet exercised), the gate is a no-op and pruning proceeds as before.

---

## STEP 3: Detect Duplicates

Compare memories pairwise within the same directory. Two memories are duplicates when:

1. **Same type** (both `feedback`, both `project`, etc.)
2. **Overlapping topic** — 60%+ of significant words (excluding stopwords) appear in both bodies
3. **Same subject** — `name` or `description` fields reference the same concept

```python
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
```

---

## STEP 4: Resolve Contradictions

Contradictions occur when two memories of the same type make opposing claims about the same subject. Detection:

1. **Same type + same topic** (overlap >= 0.4 but &lt; 0.6 — related but not duplicate)
2. **Negation signals** — one body contains negation of the other's assertion:
   - "do X" vs "do not X" / "don't X" / "never X"
   - "use X" vs "avoid X" / "stop using X"
   - "prefer X" vs "prefer Y" (for same decision domain)

```python
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
```

---

## STEP 5: Execute Changes (or Dry Run)

### Dry Run Mode (`--dry-run`)

If `--dry-run` flag is present, skip all writes. Output the full report (Step 6) with `[DRY RUN]` prefix and list what WOULD be changed:

```
[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)
```

### Live Mode

```python
# 1. Delete FULLY_STALE files
for stale in fully_stale_files:
    Bash(command=f"rm '{stale['path']}'")

# 2. Delete DUPLICATE files (keep newer)
for dup in duplicate_pairs:
    older = dup["older"]
    Bash(command=f"rm '{older['path']}'")

# 3. Delete CONTRADICTED files (keep newer)
for contradiction in contradiction_pairs:
    older = contradiction["older"]
    Bash(command=f"rm '{older['path']}'")

# 4. Rebuild MEMORY.md index from surviving files
# NOTE: `rm` above is unrecoverable (memory files are not in git). Prefer the
# trash-dir move + one-generation index rotation in references/safe-deletes.md.
```

### Rebuild MEMORY.md

Read all surviving `.md` files (excluding MEMORY.md itself). Generate the index:

```markdown
# <Directory Name> Memory

- [Name](filename.md) -- one-line description from frontmatter
```

Rules for the rebuilt index:
- One line per memory file, sorted alphabetically by filename
- **The binding constraint is BYTES, not lines.** MEMORY.md is loaded every session and stops
  loading past the read limit (~24 KB), at which point the whole index silently degrades. Line
  count is a proxy that misses this: a 151-entry index at a 147-char mean is 22.5 KB and nearly
  dead, while the same 151 entries at 112 chars is 16.2 KB and healthy.
- Target **≤ 17 KB total**. Derive the per-line budget rather than hardcoding it:
  `budget_chars = (17 * 1024 - non_entry_overhead) / entry_count`
- If the rebuild exceeds the target, **trim hooks to the derived budget before dropping any entry**.
  Truncate at a word boundary and keep the leading clause (it carries the discriminating detail).
  Every memory file must remain represented 1:1 — verify `indexed == files_on_disk` after writing.
- Only if trimming to ~90 chars still overflows should you warn the user. Never auto-delete a memory
  to fit the index; the index is a pointer table, and shrinking it is a formatting problem, not a
  retention one.

```python
# 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 6: Report

Output a summary table after consolidation:

```
## 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 |
```

If `--dry-run`, prefix the entire report with:

```
[DRY RUN] No files were modified. Run without --dry-run to apply changes.
```

---

## Error Handling

| Condition | Response |
|-----------|----------|
| No memory directories found | Report "No memory directories found" and exit |
| No memory files in directory | Report "Directory empty, nothing to consolidate" |
| All memories are FRESH | Report "All N memories are current, nothing to prune" |
| MEMORY.md exceeds 200 lines after rebuild | Warn user, do not auto-truncate |
| File deletion fails | Report error, continue with remaining files |
| Memory file has no frontmatter | Treat as EVERGREEN (cannot verify refs without metadata) |

---

## STEP 7: Plugin Housekeeping (CC 2.1.121+, #1544)

After memory consolidation, check for orphaned auto-installed plugin dependencies and offer to prune them:

```bash
# Detect orphans
claude plugin list --json | jq '[.[] | select(.auto_installed == true and .reason_kept == "orphaned")] | length'

# If > 0 and last prune > 7 days ago (track in .claude/state/last-prune.txt):
claude plugin prune  # interactive — confirms before removing
```

Skip this step on CC &lt; 2.1.121. The state file `.claude/state/last-prune.txt` records the last successful prune date so we don't run it on every dream invocation.

---

## STEP 8: Stale Project State Hint (CC 2.1.126+, #1582, fixed in #1587)

After plugin housekeeping, surface a non-blocking suggestion when stale project state exists. Never execute the purge — only preview it.

```bash
# Skip on CC < 2.1.126 (no `claude project purge` available)

# Detect stale projects via the authoritative source: `claude project purge --dry-run --all`
# emits `config: projects["<canonical-path>"]` lines that come straight from ~/.claude.json.
# Parsing these is lossless; the directory-name encoding under ~/.claude/projects/ is NOT
# (both `/` and `.` collapse to `-`, so it cannot be reversed deterministically).
stale_count=$(claude project purge --dry-run --all 2>/dev/null \
  | grep -oE 'projects\["[^"]+"\]' \
  | sed -E 's/^projects\["//; s/"\]$//' \
  | while IFS= read -r p; do
      [ -n "$p" ] && [ ! -d "$p" ] && echo "$p"
    done | wc -l)

# If > 0, surface the hint in the dream summary (never auto-execute)
if [ "$stale_count" -gt 0 ]; then
  echo "ℹ $stale_count stale project state entries detected."
  echo "   Preview cleanup with: claude project purge --dry-run --all"
fi
```

**Strict rules:** always `--dry-run`, never `--yes`. Users who moved (not deleted) a project need to keep the directory; the purge is irreversible. Surface the suggestion, let the user decide.

**Why parse `claude project purge --dry-run --all` instead of `~/.claude/projects/`:** the directory naming under `~/.claude/projects/` is a lossy collapse of the original path (`/` and `.` both become `-`). A naive `sed 's|-|/|g'` decode misidentifies any path containing `-` (e.g. `my-project` → `/my/project`). The CLI's dry-run output reads canonical paths from `~/.claude.json` and is the only reliable source.

---

## STEP 9: Cross-Repo Promotion Candidates (#3295)

A memory pattern that shows up in **2+ projects** is a capability that outgrew its repo.
While consolidating, detect these deterministically and **offer** promotion -- dream never
moves content itself, so this step stays safe when dream is model-invoked.

```bash
# 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+ distinct projects match** -> the memory is a promotion candidate.
- Deterministic only: match on normalized slug/title tokens, never on semantic judgment.
- False positives are cheap (the user declines); silent misses are the failure mode this
  step exists for -- the same infra lesson re-learned per repo, N times, with nothing watching.

**Interactive runs:** AskUserQuestion per candidate (batch when more than 3):
- "Promote to a shared plugin" -- org-specific patterns go to the org's private plugin,
  generic ones to a public plugin; dream only opens the door, the user routes.
- "Keep local" -- legitimately repo-specific overlap.
- "Stop suggesting this one" -- append `promotion: declined` to the memory's frontmatter
  metadata so future runs skip it.

**Non-interactive / dry runs:** list candidates in the Dream Consolidation Report under
`Promotion candidates:` with the matching project paths. No prompt, no mutation.

---

## When NOT to Use

- To **store** new decisions -- use `/ork:remember`
- To **search** past decisions -- use `/ork:memory search`
- To **load** context at session start -- use `/ork:memory load`
- After fewer than 5 sessions -- memory files are unlikely to have accumulated enough staleness

---

## Related Skills

- `ork:remember` -- Store decisions and patterns (write-side)
- `ork:memory` -- Search, load, sync, visualize (read-side)


---

## References (1)

### Safe Deletes

# Safe deletes and index rotation

Two recovery mechanisms for STEP 5. Both exist because of the same fact stated in
STEP 2d: **memory files are not in git, so a wrong delete is silent and
unrecoverable.** Every guard upstream of STEP 5 reduces how often a wrong delete
happens. Neither of these reduces that; they make the wrong delete survivable.

## 1. Delete to a trash directory, never `rm`

The Live Mode block deletes with `rm` in three places (fully-stale, duplicate,
contradicted). Replace each with a move into a dated trash directory under the
memory dir.

```python
from datetime import date
trash = f"{memory_dir}/.trash/{date.today().isoformat()}"
Bash(command=f"mkdir -p '{trash}'")

# Was: Bash(command=f"rm '{path}'")
Bash(command=f"mv '{path}' '{trash}/'")
```

Rules:

- **One directory per run date**, so a bad run is one directory to inspect and
  the blast radius of any single dream is legible.
- **Never `mv` over an existing name.** Two runs on the same day can select the
  same filename; `mv -n` refuses silently, which is the failure mode this whole
  file exists to prevent. Test first and suffix on collision:
  `[ -e "$dest" ] && dest="$\{dest%.md\}.2.md"`.
- **Sweep on the NEXT run, not this one.** Deleting a 7-day-old trash directory
  at the start of a run means a session that dreams twice in one day still has
  yesterday's safety net. Sweeping at the end means a crash mid-run leaves the
  net in place, which is the correct bias.
- `.trash/` must be excluded from the file walk in STEP 1, or the next run will
  re-triage everything it just deleted and conclude the memory dir is full of
  duplicates. This is the single most likely way to get this wrong.

Sweep, at the START of a run:

```python
Bash(command=f"find '{memory_dir}/.trash' -maxdepth 1 -type d -mtime +7 -exec rm -rf {{}} +")
```

## 2. Rotate MEMORY.md before overwriting it

The rebuild ends in a single-shot `Write(path=".../MEMORY.md", ...)`. That write
is the one irreversible step in the whole skill: it replaces the pointer table for
every surviving memory in one operation, and a truncated or mis-generated index
silently degrades every later session rather than failing loudly.

```python
# BEFORE the Write, keep exactly one generation back.
Bash(command=f"[ -f '{memory_dir}/MEMORY.md' ] && cp '{memory_dir}/MEMORY.md' '{memory_dir}/.MEMORY.md.prev'")
Write(path=f"{memory_dir}/MEMORY.md", content=rebuilt_index)
```

One generation is deliberate, not a compromise:

- The failure this catches is **"the rebuild I just ran was wrong"**, which is
  noticed within the same session or the next one. A deeper history answers a
  question nobody asks.
- `MEMORY.md` is loaded every session, so a `.MEMORY.md.*` family in the same
  directory risks the walker picking up a rotation as a memory file. One
  dot-prefixed fixed name has no such ambiguity.

### Verify the rotation is a rotation, not a copy of the damage

Rotating **after** a bad write preserves the bad write. Order matters, so assert
it: the copy must happen while the old bytes are still on disk. A cheap check
that catches the inverted order is that `.MEMORY.md.prev` and `MEMORY.md` must
differ whenever the entry count changed.

```python
prev = int(Bash(command=f"grep -c '^- \\[' '{memory_dir}/.MEMORY.md.prev' 2>/dev/null || echo 0"))
now  = int(Bash(command=f"grep -c '^- \\[' '{memory_dir}/MEMORY.md'"))
```

Note the `|| echo 0` there is safe **only** because a missing prev file on a
first-ever run is genuinely zero. Do not copy that idiom to the `now` read: a
`grep -c` failure there would report an empty index as a successful one, which is
exactly the silent-degradation class this section is guarding against.

## What neither of these fixes

Both are recovery, not prevention. They do nothing about a memory that was
correctly deleted but should not have been written in the first place, and
nothing about an index that is well-formed but wrong. The STEP 2d staleness
guards remain the primary control; this file is the seatbelt, not the brakes.
