Layered Memory Manager
v1.0.3Multi-tier (L1/L2) memory management skill for OpenClaw agents. Use when: (1) reading, writing, organizing, or searching memories, (2) deciding what to remem...
Like a lobster shell, security has layers — review code before you run it.
layered-memory-manager
Startup 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 Architecture
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 Responsibilities
| 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) |
Retrieval Strategy
These are behavioral guidelines, not rigid procedures. The goal is disciplined recall habits, not mechanical layering.
Primary: Global memory_search
Use memory_search as the primary recall tool — it's fast and covers all layers.
Secondary: Layer Awareness
When memory_search returns uncertain or partial results:
- Check MEMORY.md L1 hot cache for confirmed facts
- Use
grep_searchonmemory/*.mdfor precise keyword or pattern matches in L2 (faster than full file reads) - Check L2 file for full context
- Check daily logs if still unresolved
When to Use memory_get
memory_searchorgrep_searchgave uncertain results and you need to verify exact content- You know the file/location from context
- Confirming L1↔L2 sync state during hygiene check
Behavioral Rules
- Never assume — always run
memory_searchbefore answering memory questions - Pre-decision Self-check (Mandatory) — Before executing any tool that modifies the environment (e.g.,
run_shell_command,replace), must perform a quickmemory_searchorgrep_searchfor relevant preferences or previous decisions. Never rely on model defaults if a project-specific memory might exist. - L1/L2 is binding — L1 is a derived cache of L2; they are never independent. Any change to content always goes L2 first, then L1 resyncs. There is no such thing as editing L1 only.
- Think in layers, not file scanning — know which layer holds what; check L1/L2 when search gives weak signals
L2 → L1 Promotion Mechanism
Triggers
Promotion to L1 happens when content meets any of:
- Access frequency —
hygiene.jsonaccessLog[L2-key].sessions.length >= 3(3+ unique sessions have accessed this L2 entry) - Context criticality — content is essential for every session (e.g., user name, core principles)
- User explicitly requests — user says "remember this", "always keep in mind"
- Rapid re-access — the same L2 content is needed twice in one session
Promotion Process
- Read full content from L2 file
- Write condensed inline version to MEMORY.md under appropriate layer section
- Keep L2 unchanged (source of truth stays intact)
- Add promotion metadata tag to the L1 entry (see Tag Format below)
- Remove the entry from
accessLog— L2→L1 promotion is terminal for that L2 key; L1 access is tracked via the L1 section directly (promotionLog captures the event) - After write: verify sync — immediately read back the L2 file and confirm L1 condensation is still accurate. If L2 was edited mid-session, update or remove the L1
↑tag and re-promote fresh.
Condensation Rules for L1
L1 inline content should be:
- ≤ 30 bullets total across all layers (hard cap — see §L1 Global Budget)
- Per-layer is a soft target (~3–5 bullets) — not a hard ceiling; stay within global budget
- Most representative points only
- No context or reasoning — just facts/decisions
- If content can't be condensed, prioritize the top entries and note
→ Full version: memory/<layer>.md
Promotion Metadata Tag
Every 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.jsonsessionslist(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 pressure
Examples:
↑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-synced
Tag 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).
L1 Global Budget & Eviction Priority
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.
Example: Promoting a Decision
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`
L1 → L2 Demotion Mechanism
Observable Triggers
Demotion from L1 happens when ANY of the following is directly observable:
- Zero-access demotion — this L1 entry was NOT queried (
memory_searchhit it) for the last 3 consecutive sessions - Size overflow — L1 total exceeds ~30 bullets; demote lowest-priority entries
- User explicitly updates preference — user changes something; update L2, then sync L1
- Context expiry — an entry was promoted for a temporary context (e.g., project X) and that context is now finished
Note 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.
Tracking: sessionsSinceAccess (L1 entries)
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):
- Call
session_statusto get the current session ID - For each L1 entry in
L1accessLog:- If
lastSessionIdis NOT equal to the current session ID →sessionsSinceAccess++ - If
sessionsSinceAccess >= 3→ mark for demotion (❄️ cold tier)
- If
- Save updated
hygiene.json
On 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).
Demotion Process
- Do NOT delete L2 — L2 is always the source of truth
- Remove the inline content from MEMORY.md hot cache section
- Add a reference note in MEMORY.md layer section pointing to full L2 content
- Log the demotion in
hygiene.jsondemotionLog - Update
L1accessLog— remove the entry (it's now cold) - Re-initialize
accessLog— the content is back in L2 cold storage withaccessCount: 0, sessions: [], lastAccess: null(promotion history lives inpromotionLog; re-promotion will trigger naturally from fresh accesses)
Demotion ≠ Deletion
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.
Staleness Detection
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.
L2 Key Format
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)- Example:
memory/decisions.md:two-tier-architecture
This format lets you directly map any hygiene log entry back to its source file.
hygiene.json Schema
{
"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.
Manual Tag Triggers
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).
- Add
↑[pin]to the entry's metadata tag, e.g.↑2026-04-22(3 sessions)←memory/decisions.md[pin] - Persist
↑[pin]in both L1 and L2 source - Update
L1accessLog[entry].lastAccess = today - Layer index:
memory/<layer>.md:<slug>
[[promote:<layer>:<slug>]]
Force-promote a L2 entry to L1 immediately (bypasses 3-session threshold).
- Read full content from L2
- Condense and write to L1 with
↑YYYY-MM-DD(user request)←<L2-source> - Log to
promotionLog - Remove from
accessLog(or reset: accessCount → 0, sessions → [], lastAccess → null)
[[forget:<layer>:<slug>]]
Demote a L1 entry back to L2 cold storage immediately.
- Remove L1 inline content
- Keep L2 source intact
- Log to
demotionLog - Remove from
L1accessLog - Re-initialize
accessLogfor 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:
- Case-insensitive (
jarvismatchesJarvis,JARVIS) - Substring match on both entry slug and full content
- Matched as continuous substrings (e.g.,
two-tiermatchestwo-tier-architecture, nottwoalone) - Delimiters (space,
-,_,.) are included but do not break the match - L1 bullet matched → demote that entry immediately
- L2 file content matched → also demote the corresponding L1 entry if one exists
- Report how many entries were forgotten after completing the scan
[[restore:<layer>:<slug>]]
Restore an archived entry back to active L2 storage.
- Search
memory/archive/for the entry matching the slug. - Re-insert the content into
memory/<layer>.md. - Remove from
archiveQueueinhygiene.json. - Initialize
accessLog[entry]withaccessCount: 1(to account for the restoration access). - Optional Promotion: If the user specifies (e.g., "restore and pin"), also promote to L1.
[[memory_health]] — Status Snapshot
Called 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.
Archive / Forgetting Mechanism
Archive Triggers
- Cold L2: use the earlier of file creation date and last access date as the baseline:
- If
lastAccess == nulland file mtime is 30+ days old → archive candidate - If
lastAccess != nulland days sincelastAccess >= 30→ archive candidate - If
accessCount == 0(never accessed at all) and file is new, no action yet — wait for the 30-day window to close before treating as cold
- If
- Post-demotion cold storage: an entry was demoted from L1 and has had 0 re-access for 60 days in L2
- User explicitly discards: user says "forget about X" or "delete X" → move to archive, never delete outright
Archive Process
- Move content from
memory/<layer>.mdtomemory/archive/<layer>-<date>.md - Add entry to
hygiene.jsonarchiveQueuewith{entry, archivedAt, reason} - Update
memory/<layer>.md— remove the content, add a comment noting it's archived - Log to
memory/decisions.mdor today's daily log - Proactive Search Notice: If future tasks trigger an archive search, notify the user about the existence of relevant archived entries.
Archive Review & Log Pruning
During heartbeat hygiene, if archiveQueue.length > 0, check each item:
- If archived > 180 days ago and never restored → permanently delete from archive (optional, user can confirm)
- Otherwise leave in archive indefinitely
Hygiene Log Pruning: To prevent hygiene.json from becoming a performance bottleneck:
- Prune
promotionLoganddemotionLogentries older than 180 days. - Ensure the file is kept under ~50KB. If larger, archive old log entries into
memory/archive/hygiene-log-YYYY-MM.json.
Write Strategy (Cache Coherence)
Write Order (Enforced)
- Write to L2 first — L2 is always source of truth
- Sync L1 immediately — update hot cache to match L2, add new
↑tag with today's date and reason - Update hygiene.json — update the relevant accessLog (L1accessLog or accessLog), reset counters
Sync Verification
Write L2 first, then sync L1. Because they are binding, this order is always enforced:
- Write new/updated content to L2 (source of truth)
- Immediately re-condense and update the corresponding L1 entry with
↑today(<reason>)←<L2-source> - Verify: read back L2 and confirm L1 condensation is accurate
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>
When to Activate This Skill
Before Answering Memory Questions
Any question about past sessions, decisions, preferences, or facts → memory_search first, then L1/L2 as needed.
After Significant Events
- User says "remember this" or "don't forget"
- Made a decision that should persist
- Learned something new about the user or environment
- Completed a project or milestone
- Opened or closed a project / context
Manual Tag Triggers (user-embedded)
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.
When Creating or Editing Skills
Align new skill structure with memory architecture. Update memory/skills.md when skills are installed or removed.
During Hygiene Maintenance
Run the Hygiene Checklist (defined below) when maintenance is needed.
Hygiene Checklist
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
Recovery: Rebuilding hygiene.json
If hygiene.json is lost or corrupted:
- Read MEMORY.md — extract all L1 entries with their
↑tags → rebuildL1accessLog(all entries start withsessionsSinceAccess: 0) - Read all L2 files → for each entry, initialize
accessLogwithaccessCount: 0, sessions: [], lastAccess: null - Preserve
promotionLoganddemotionLogfrom the corrupted file if any survived; if fully gone, reconstruct from L1↑tags and daily logs - Recreate hygiene.json with recovered data + current date
Known 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.
If MEMORY.md is lost:
- Read all L2 files → rebuild L1 by condensing the most important content (≤ 5 bullets per layer, priority: Identity > User > Preferences > Decisions > Knowledge > Skills > Context)
- Use
promotionLogto re-promote known-hot entries first - Restore
L1accessLogwith allsessionsSinceAccess: 0
Critical Rules
- MEMORY.md is private — never load or mention in group chats
- L1/L2 are binding — no such thing as editing one without the other. Write L2 first, then sync L1. Never edit L1 directly.
- L2 is source of truth — never delete L2 content directly; archive first
- Keep L1 and L2 in sync — the
↑tag date is always the last sync date; never leave a stale tag - Promote at accessCount >= 3 — use the counter, not guesswork
- Tag everything — no untagged L1 entries;
↑metadata is mandatory - Log all changes — hygiene.json tracks everything; no untagged state changes
- Archive, don't delete — cold content goes to
memory/archive/, not trash
Storage Patterns
Adding a New Layer
- Create
memory/<layer>.md - Write full content to L2
- Condense and write the L1 entry in MEMORY.md with
↑YYYY-MM-DD(new layer)←memory/<layer>.md - Update MEMORY.md Layer Index
- Initialize
accessLog[<layer>:<slug>]in hygiene.json (accessCount: 0, lastAccess: null)
Daily Session Logging
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.
Archive Directory
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.
Comments
Loading comments...
