guide · computers

Daily Memory Audit and the 4-Step Merge Plan: From Cron Flag to Clean Memory

Operator playbook for the nightly memory-audit cron: the 4-step merge plan (read / propose / dedupe / commit), the 3 categories of stale memory, and the 5 traps that look mergeable but are not.

July 15, 2026 · By Alastair Fraser

A friendly retro robot standing at a workbench under a clock that reads 00:30, sorting a small stack of memory cards into three labeled bins — KEEP, MERGE, DROP — while a clipboard checklist titled 4-STEP MERGE PLAN sits beside the bin.

The daily memory audit fires every night at 00:30 UTC and writes a flag list to the morning’s report. The discipline that turns the flag list into a clean MEMORY.md — or another day of ignored Telegram pings — lives in the next morning’s first session. This guide is the playbook for that session: how to read the flag, how to walk the 4-step merge plan, what the 3 categories of stale memory look like, and the 5 things that look mergeable but are not.

Companion guides established the rubric (Storage Decision) and the scoping rules (ABS Chat with Skills and Memory). This one closes the loop.

What the cron actually does

memory_audit.sh at /opt/data/scripts/memory_audit.sh reads MEMORY.md and USER.md and emits a flag list. It does not edit anything. Checks, in order:

  • Char count against the per-profile ceiling (2,200 for MEMORY.md, 1,375 for USER.md on this profile).
  • Entry count above 5, or any single entry above 400 chars.
  • Date stamps (2026-, 2025-, full and abbreviated month names).
  • History-pattern words: fixed, shipped, v1., v2., vN, postmortem, pytest, migrated, rolled out, launched, deprecated, rebuilt, replaced, upgraded, audit, version.
  • UTF-8 validity — silently skips binary garbage rather than failing loudly.

Cron job ID: 81c7c004205e, schedule 30 0 * * *, no_agent: true. Output: /opt/data/cron/output/81c7c004205e/<date>.md. The job surfaces; the shrink is yours, in the morning’s first session.

The 4-step merge plan

Each flag becomes a candidate. The plan turns a candidate list into a smaller, sharper MEMORY.md without losing what actually matters. The four steps, in order:

Step 1 — Read

Pull the most recent audit run and read live state — never yesterday’s snapshot:

cat $(ls -t /opt/data/cron/output/81c7c004205e/*.md | head -1)
memory(action='read')
memory(action='read', target='USER.md')

Re-read after the proposal pass to confirm; the previous session may have already trimmed one entry.

Step 2 — Propose

Apply the 3-question filter from memory-hygiene to each flagged entry:

  1. Will I need this verbatim at session start, before I can read any file?
  2. If I forgot it, would I make a wrong decision or repeat a mistake?
  3. Is it a rule, an environment fact, or a stable preference — not an event, version, or fix?

A no on any question is a routing signal, not a deletion:

CategoryDestination
Procedural with stepsSkill (skill_manage(action='patch'))
Entity-tagged factfact_store (when registered) or one-line file under /opt/data/facts/
Cross-session detailHandoff at /opt/data/notes/session_handoff_<date>.md
Version progressiongit log + skill reference, never memory
Burn-prevention lineKeep verbatim in MEMORY.md

Each proposal is one entry: KEEP / TRIM / MOVE / REMOVE, with destination, character delta, retrieval test, and risk. Proposal-only is the contract. Stop and wait for explicit approval.

Step 3 — Dedupe

Run the redundancy test against the remaining memory, not just flagged entries. From harness-self-maintenance:

  1. Does this already exist in a skill I can skill_view?
  2. Does it live in a cron job’s prompt or attached skill?
  3. Does it belong in fact_store as an entity-tagged fact?
  4. Does it belong in a session handoff note?
  5. If none of the above: is it a burn-prevention line?

Two overlapping entries collapse into one. Keep the more recent date marker and the more specific wording; route the older one to its destination.

Step 4 — Commit

Apply approved operations in one atomic memory batch where possible:

memory(action='batch', operations=[
    {'action': 'replace', 'old_text': '<exact substring of entry A>', 'content': '<new shorter entry A>'},
    {'action': 'replace', 'old_text': '<exact substring of entry B>', 'content': '<new shorter entry B>'},
    {'action': 'remove',  'old_text': '<exact substring of entry C>'},
])

Then verify both files sit below the ceiling. If not, the audit will fire again tonight — that is correct, not a failure. Re-run the redundancy test on whatever still flagged and propose the next pass.

The 3 categories of stale memory

The flag list looks like noise until you sort it into three shapes. Each has a different destination.

1. Procedural that became a skill

The entry started as “how to deploy the Astro site end-to-end.” Then astro-static-site-build-and-deploy-end-to-end became a skill. The memory line is a duplicate, loaded every turn, costing chars for nothing. Destination: delete from memory, keep the skill. Optional one-line pointer: “Astro deploy recipe: see skill astro-static-site-build-and-deploy-end-to-end.”

2. User-fact that drifted

The entry was true when written. It is no longer true. Verified on this profile: store count dropped from 4 to 1 (NATT only); the Daily Memory Audit cron ID changed once; the MEMORY.md ceiling moved from 2,000 to 2,200 chars. The audit catches drift by date markers and history-pattern words, not by content alone. Destination: rewrite to current truth in MEMORY.md, or move to the project file if no longer cross-cutting.

3. One-off event that aged out

The entry was a session record. “On 2026-07-02 we shipped parser V1.3.9 with 342 fields.” That is a git log line, not memory. The audit catches it on the 2026- date marker and the shipped + v1. words. Destination: the matching postmortem at /opt/data/<project>/docs/postmortems/<date>-<slug>.md, or the skill reference if a class-level pitfall emerged. Memory stays clean.

The script shape (memory_audit.sh)

The shape that has worked since 2026-07-02:

#!/usr/bin/env bash
# memory_audit.sh — flag-only, never mutates.
MEM="${MEMORY:-/opt/data/memories/MEMORY.md}"
USER="${USER:-/opt/data/memories/USER.md}"
CEILING_MEM=2200
CEILING_USER=1375
PATTERN='20[0-9]{2}-|fixed|shipped|v[0-9]+\.|postmortem|pytest|migrated|rolled out|launched|deprecated|rebuilt|replaced|upgraded|audit|version'

check_file() {
  local path="$1" label="$2" ceiling="$3"
  python3 -c "open('$path','rb').read().decode('utf-8')" || { echo "[$label] WARN: not valid UTF-8 — audit unreliable"; return; }
  local chars=$(python3 -c "print(len(open('$path', encoding='utf-8').read()))")
  echo "[$label] $chars/$ceiling chars ($(( chars * 100 / ceiling ))%)"
  grep -niE "$PATTERN" "$path" | awk -v l="$label" '{printf "[%s] FLAG: %s\n", l, $0}'
}

check_file "$MEM"  "MEM"  "$CEILING_MEM"
check_file "$USER" "USER" "$CEILING_USER"

Two shape notes that matter: use python3 for the char count (CJK chars are 3 bytes in UTF-8; wc -c over-reports), and validate UTF-8 before trusting any flag-free result (binary garbage makes grep silently return empty). Both pitfalls are documented in memory-hygiene v1.2.0 (2026-07-12).

What NOT to merge

Five shapes look mergeable but are traps:

  1. Skills pretending to be memory. A 40-token “always remember X” is not a skill; it is a memory entry wearing a costume. If skill_view would not load it on a matching task, fold it into MEMORY.md and delete the skill directory.
  2. Dated facts as dated. “On 2026-07-10 I decided…” is a session log. Strip the date; if the underlying rule still matters, rewrite without the timestamp.
  3. Ephemeral context. Today’s deploy path, the URL you needed once. Memory survives across sessions; one-session facts go in /opt/data/notes/session_handoff_<date>.md.
  4. Secrets and credentials. Never memory, never fact_store, never skill frontmatter. The .env file is the only path. The audit does not catch secrets — that is your job.
  5. Duplicate entries on the same topic. Two entries that say “prefer surgical execution” and “be concise, surgical” are one entry with one rewording. Dedupe before commit.

The verification checklist

python3 -c "print(len(open('/opt/data/memories/MEMORY.md', encoding='utf-8').read()))"
python3 -c "print(len(open('/opt/data/memories/USER.md',   encoding='utf-8').read()))"
ls -lt /opt/data/cron/output/81c7c004205e/ | head -3

If both char counts are below ceiling, no flags reappear within 36 hours, and cron_health_monitor reports clean, the merge plan worked. If the audit fires again the next night, the redundancy test missed something — re-run it on whatever still flagged, do not just shrink harder.

Sources

Last verified: 2026-07-15 against memory_audit.sh at /opt/data/scripts/memory_audit.sh, cron 81c7c004205e, and memory-hygiene v1.2.0.

Sources

#hermes-agent#memory#audit#cron#merge-plan#operator-rubric

Submit a take

Have a different read on this? Drop a comment below — your email isn't published, and I read every one. Nothing leaves the site until I approve it.

Your email address will not be published. Required fields are marked.