Install
openclaw skills install layered-memory-managerMulti-tier (L1/L2) memory management skill for OpenClaw agents. Use when: (1) reading, writing, organizing, or searching memories, (2) deciding what to remember or forget, (3) performing memory hygiene (L1↔L2 sync, promotion, demotion), (4) answering questions about prior sessions, decisions, or preferences. Supports explicit forget (by tag or keyword), manual pin/promote via [[tag]] triggers, and memory_health status. This is the agent's own layered memory system — the authoritative guide for where memories live and how to keep them accurate.
openclaw skills install layered-memory-managerStartup vs Hygiene mode: The description above is your startup guide. Read the full skill below only when doing hygiene, maintenance, or architecture changes — not at every session boot.
Memory is a two-tier cache system. Think L1 (hot) + L2 (cold).
MEMORY.md = L1 Hot Cache: Frequently accessed content stored inline for fast retrieval. Also serves as index to L2 files.
memory/*.md = L2 Cold Storage: Full detailed content lives here.
hygiene.json = Access Tracker: Located at memory/hygiene.json (workspace-relative). Tracks all access counts, promotions, and demotions.
MEMORY.md (L1 Hot Cache + Index)
├── [inline hot memories with metadata] ← frequently used, fast access
└── Layer Index ← pointer to L2 files
memory/*.md (L2 Cold Storage)
├── identity.md ← full identity details
├── user.md ← full user profile
├── preferences.md ← all preferences
├── knowledge.md ← stable facts, rules, experience
├── decisions.md ← decisions with context
├── skills.md ← installed skills
├── context.md ← ongoing projects
├── hygiene.json ← access tracking (NOT in a layer file)
└── YYYY-MM-DD.md ← daily session logs
memory/archive/ ← archived cold content
| Layer | L1 (Hot Cache) | L2 (Cold Storage) |
|---|---|---|
| Identity | Name, soul, core principles | Full personality details |
| User | Name, platform, language | Full user profile |
| Preferences | Core preferences | All preferences |
| Knowledge | Frequently accessed facts | Stable facts, rules, experience |
| Decisions | Recent key decisions | All decisions with context |
| Skills | Installed skills, usage principles | Detailed skill docs |
| Context | Ongoing projects | Full project details |
| Daily | — | Raw session logs |
| Hygiene | — | hygiene.json (tracked separately) |
These are behavioral guidelines, not rigid procedures. The goal is disciplined recall habits, not mechanical layering.
memory_searchUse memory_search as the primary recall tool — it's fast and covers all layers.
When memory_search returns uncertain or partial results:
grep_search on memory/*.md for precise keyword or pattern matches in L2 (faster than full file reads)memory_getmemory_search or grep_search gave uncertain results and you need to verify exact contentmemory_search before answering memory questionsrun_shell_command, replace), must perform a quick memory_search or grep_search for relevant preferences or previous decisions. Never rely on model defaults if a project-specific memory might exist.Promotion to L1 happens when content meets any of:
hygiene.json accessLog[L2-key].sessions.length >= 3 (3+ unique sessions have accessed this L2 entry)accessLog — L2→L1 promotion is terminal for that L2 key; L1 access is tracked via the L1 section directly (promotionLog captures the event)↑ tag and re-promote fresh.L1 inline content should be:
→ Full version: memory/<layer>.mdEvery L1 entry MUST carry a metadata tag. Format:
↑YYYY-MM-DD(<reason>)←<L2-source>[<flags>]
<reason> must be one of:
(N sessions) — promoted after N cross-session accesses from hygiene.json sessions list(user request) — user explicitly asked to remember(critical) — context-critical, needed every session(sync) — L2 was edited; L1 re-condensed to match new L2 content<L2-source> is the L2 key of the source file (see §L2 Key Format), e.g. memory/decisions.md. This field is mandatory — no tag is valid without it.
<flags> is optional; omit if none apply. Supported flags:
[pin] — manually pinned via [[pin:<layer>:<slug>]]; never demoted by budget pressureExamples:
↑2026-04-22(3 sessions)←memory/decisions.md — promoted after 3 cross-session accesses↑2026-04-22(user request)←memory/preferences.md[pin] — promoted and manually pinned↑2026-04-22(critical)←memory/identity.md — context-critical, needed every session↑2026-04-22(sync)←memory/decisions.md — L2 was edited; L1 re-syncedTag integrity rule: Because L1 and L2 are bound, the ↑ tag date is the date of the last L2→L1 sync. After any L2 edit, the corresponding L1 tag must be updated to today's date with reason (sync).
Hard cap: ≤ 30 bullet points total across all hot layers.
Entries are evicted in priority order — lowest priority evicted first:
| Priority | Label | Rule | Never demoted |
|---|---|---|---|
| 1 | 🔒 critical | Tagged ↑(critical) by system | ✅ yes |
| 2 | 📌 pinned | Tagged ↑[pin] manually or via [[pin:<layer>:<slug>]] | ✅ yes |
| 3 | 🔥 recent | sessionsSinceAccess == 0 (accessed this or last session) | ❌ no |
| 4 | ⏳ stale | sessionsSinceAccess == 1–2 (skipped 1–2 sessions) | ❌ no |
| 5 | ❄️ cold | sessionsSinceAccess >= 3 OR tagged [[forget:<layer>:<slug>]] | ❌ no |
Eviction order: cold → stale (oldest first) → recent (oldest first). Tiers 1–2 are exempt.
During overflow, demote lowest-priority entries first. If priority ties, demote the one with the highest sessionsSinceAccess. If still tied, demote the oldest by ↑ promotion date.
L2 memory/decisions.md:
- 2026-04-22: Restructured memory into two-tier architecture
- Reason: Layering makes retrieval faster, MEMORY.md acts as hot cache
- Discussion: User proposed this design, I agreed with the approach
L1 MEMORY.md Decisions section:
## 📋 Decisions (Hot Cache)
- 2026-04-22: Restructured memory into two-tier architecture (hot cache + L2) ↑2026-04-22(3 sessions)←memory/decisions.md
→ Full version: `memory/decisions.md`
Demotion from L1 happens when ANY of the following is directly observable:
memory_search hit it) for the last 3 consecutive sessionsNote on staleness: Because L1/L2 are always bound, staleness is not a separate demotion trigger — it is handled automatically via sync. If L2 changes, L1 always resyncs immediately (not demoted). The
↑tag date reflects the last sync.
L1 entries are tracked in hygiene.json L1accessLog:
{
"L1accessLog": {
"<layer>:<slug>": {
"sessionsSinceAccess": 0,
"lastAccess": null,
"lastSessionId": null,
"pinned": false
}
}
}
Session-start increment (mandatory, every new session):
session_status to get the current session IDL1accessLog:
lastSessionId is NOT equal to the current session ID → sessionsSinceAccess++sessionsSinceAccess >= 3 → mark for demotion (❄️ cold tier)hygiene.jsonOn every L1 entry access (any memory_search hit or direct read within the current session):
→ L1accessLog[entry].sessionsSinceAccess = 0
→ L1accessLog[entry].lastAccess = <today>
→ L1accessLog[entry].lastSessionId = <current session ID>
Key: A single long session accessing the same entry 50 times counts as 1 session of access, not 50. Entries not accessed during the previous session are the only ones that accumulate sessionsSinceAccess.
L2 entries use accessLog (separate tree):
{
"accessLog": {
"<L2-key>": {
"accessCount": 0,
"sessions": [], // unique session IDs that accessed this L2 entry
"lastAccess": null
}
}
}
On L2 access: if sessionId not already in sessions, push it → accessCount = sessions.length, lastAccess = today. The promotion trigger checks accessCount >= 3, where each increment requires a different session — a single session accessing the same L2 entry 10 times still counts as 1.
On L2→L1 promotion: remove from accessLog (promotion is terminal).
hygiene.json demotionLogL1accessLog — remove the entry (it's now cold)accessLog — the content is back in L2 cold storage with accessCount: 0, sessions: [], lastAccess: null (promotion history lives in promotionLog; re-promotion will trigger naturally from fresh accesses)Demotion means "remove from hot cache", not "delete". L2 always retains the full version. The goal is keeping L1 lean and accurate, not reducing total memory.
Because L1/L2 are always binding, staleness is handled by sync, not demotion:
FOR each L1 layer section:
READ L2 file for that layer
FOR each L1 bullet with ↑YYYY-MM-DD tag:
IF L2 was modified AFTER ↑date:
→ re-condense L2 content → update L1 with ↑today(sync)←<L2-source>
ELSE IF L2 has content not in L1:
→ re-condense and promote (add ↑today(sync))
Only entries that are genuinely cold (3+ sessions with no access) should be demoted — not ones whose L2 source changed.
Every entry in accessLog and promotionLog / demotionLog uses the key format:
memory/<layer>.md:<slug>
<layer> is the layer name (e.g. decisions, preferences)<slug> is a short, unique identifier for the entry within that file — use a URL-safe slug derived from the entry topic or first line (e.g. two-tier-architecture, preferred-package-manager)memory/decisions.md:two-tier-architectureThis format lets you directly map any hygiene log entry back to its source file.
{
"L1accessLog": {
"<layer>:<slug>": {
"sessionsSinceAccess": 0,
"lastAccess": null,
"lastSessionId": null,
"pinned": false
}
},
"accessLog": {
"<L2-key>": {
"accessCount": 0,
"sessions": [],
"lastAccess": null
}
},
"promotionLog": [
{ "entry": "<L2-key>", "from": "L2", "to": "L1", "at": "YYYY-MM-DD", "reason": "..." }
],
"demotionLog": [
{ "entry": "<L2-key>", "from": "L1", "to": "L2", "at": "YYYY-MM-DD", "reason": "..." }
],
"archiveQueue": []
}
Note:
hygiene.jsonlives atmemory/hygiene.json(workspace-relative), NOT inside the skill directory.
User can embed directive tags in messages. These are detected and acted upon during session processing — no cron needed.
[[pin:<layer>:<slug>]]Pin a L1 entry so it becomes Tier 2 (never demoted by budget pressure).
↑[pin] to the entry's metadata tag, e.g. ↑2026-04-22(3 sessions)←memory/decisions.md[pin]↑[pin] in both L1 and L2 sourceL1accessLog[entry].lastAccess = todaymemory/<layer>.md:<slug>[[promote:<layer>:<slug>]]Force-promote a L2 entry to L1 immediately (bypasses 3-session threshold).
↑YYYY-MM-DD(user request)←<L2-source>promotionLogaccessLog (or reset: accessCount → 0, sessions → [], lastAccess → null)[[forget:<layer>:<slug>]]Demote a L1 entry back to L2 cold storage immediately.
demotionLogL1accessLogaccessLog for that entry (accessCount: 0, sessions: [], lastAccess: null)[[forget keyword:<word>]]Forget any entry whose slug or content contains <word> — case-insensitive substring match across L1 and L2.
Match semantics:
jarvis matches Jarvis, JARVIS)two-tier matches two-tier-architecture, not two alone)-, _, .) are included but do not break the match[[restore:<layer>:<slug>]]Restore an archived entry back to active L2 storage.
memory/archive/ for the entry matching the slug.memory/<layer>.md.archiveQueue in hygiene.json.accessLog[entry] with accessCount: 1 (to account for the restoration access).[[memory_health]] — Status SnapshotCalled on demand (not on heartbeat). Output format:
=== Memory Health ===
L1: <N>/30 bullets | <M> tagged [pin]
L2: <X> files | <Y> entries tracked
Promotions (total): <P>
Demotions (total): <D>
Archive queue: <A> items
===
Priority breakdown:
🔒 critical: <n> 📌 pinned: <n> 🔥 recent: <n> ⏳ stale: <n> ❄️ cold: <n>
===
L2 cold candidates (never accessed, age>30d): <C>
L1↔L2 sync (L1 has stale L2 source): <s>
===
Log Cleanup:
Log items over 180 days pruned from hygiene.json: <L>
===
Top L1 entries by sessionsSinceAccess:
1. <layer>:<slug> — <n> sessions stale
2. ...
Note: L2 cold candidates measures L2 entries that have never been accessed (accessCount==0) and are older than 30 days — candidates for archive. L1↔L2 sync measures L1 entries whose L2 source has been modified since promotion — these need re-evaluation.
lastAccess == null and file mtime is 30+ days old → archive candidatelastAccess != null and days since lastAccess >= 30 → archive candidateaccessCount == 0 (never accessed at all) and file is new, no action yet — wait for the 30-day window to close before treating as coldmemory/<layer>.md to memory/archive/<layer>-<date>.mdhygiene.json archiveQueue with {entry, archivedAt, reason}memory/<layer>.md — remove the content, add a comment noting it's archivedmemory/decisions.md or today's daily logDuring heartbeat hygiene, if archiveQueue.length > 0, check each item:
Hygiene Log Pruning: To prevent hygiene.json from becoming a performance bottleneck:
promotionLog and demotionLog entries older than 180 days.memory/archive/hygiene-log-YYYY-MM.json.↑ tag with today's date and reasonWrite L2 first, then sync L1. Because they are binding, this order is always enforced:
↑today(<reason>)←<L2-source>After any L2 update:
READ MEMORY.md corresponding layer
IF L1 section does NOT reflect the change:
→ re-condense L2 content and write to L1 with ↑today(sync)←<L2-source>
Any question about past sessions, decisions, preferences, or facts → memory_search first, then L1/L2 as needed.
Detect and process [[pin:<layer>:<slug>]], [[promote:<layer>:<slug>]], [[forget:<layer>:<slug>]], [[forget keyword:<word>]], or [[memory_health]] tags in user messages — act on them during the same turn.
Align new skill structure with memory architecture. Update memory/skills.md when skills are installed or removed.
Run the Hygiene Checklist (defined below) when maintenance is needed.
Run during periodic maintenance. Keep it light — focus on what actually changed since last run:
1. CHECK L1 budget: count bullets — if approaching ~30:
demote ❄️ cold → ⏳ stale (oldest first) → 🔥 recent (oldest first)
skip 🔒 critical and 📌 pinned entries regardless of age
2. SYNC check: for each L1 entry with ↑ tag, compare ↑date with L2 mtime
- L2 newer than ↑date → re-condense L2 and update L1 tag to ↑today(sync)
- L2 has new content not in L1 → re-condense and promote (add ↑today(sync))
(Note: no demotion here — L2 being newer means L1 needs to catch up, not go away)
3. CHECK accessLog:
- Any L2 entry `accessCount >= 3` (check `sessions.length`) and still in L2? → promote to L1
4. CHECK archiveQueue:
- Any L2 entry cold for 30+ days? → move to memory/archive/
- Any archived > 180 days? → surface for permanent-deletion confirmation
5. CHECK post-demotion cold storage:
- Any demoted L1 entry in L2 with 0 re-access for 60+ days? → add to archiveQueue
6. SAVE hygiene.json with all updated logs
hygiene.json is lost or corrupted:↑ tags → rebuild L1accessLog (all entries start with sessionsSinceAccess: 0)accessLog with accessCount: 0, sessions: [], lastAccess: nullpromotionLog and demotionLog from the corrupted file if any survived; if fully gone, reconstruct from L1 ↑ tags and daily logsKnown recovery cost: All cross-session access history resets to 0. L2 entries approaching the 3-session promotion threshold must re-accumulate from scratch. Accept this as unavoidable without persistent session logs.
MEMORY.md is lost:promotionLog to re-promote known-hot entries firstL1accessLog with all sessionsSinceAccess: 0↑ tag date is always the last sync date; never leave a stale tag↑ metadata is mandatorymemory/archive/, not trashmemory/<layer>.md↑YYYY-MM-DD(new layer)←memory/<layer>.mdaccessLog[<layer>:<slug>] in hygiene.json (accessCount: 0, lastAccess: null)Write to memory/YYYY-MM-DD.md. Use this minimal template:
# YYYY-MM-DD
## Summary
<!-- 3-5 summary items -->
## Key Outcomes
<!-- Decisions, commitments, and things to remember -->
## Notes
<!-- Items worth recording but not yet ready for L2 promotion -->
After the session ends, distill key outcomes into L2 layer files and sync important content to L1 as needed.
memory/archive/
├── decisions-2026-01-15.md ← cold decision, archived
├── context-projectX-2026-03-01.md
└── ...
Archive files keep the original content intact for potential restoration.