An agent's memory lives in files and databases that its framework rewrites freely. ALF backs that memory up as a chain of small, precise deltas. This page walks the mechanism end to end — with real example data for OpenClaw, ZeroClaw, and Hermes — as it works with base-aware reconciliation (WP4.1).
ALF doesn't diff lines like git does. Each framework adapter first turns the
agent's memory store into memory records — for markdown stores, one record per
## section; for database stores, one record per row. Every record has a stable
identity (a UUID) and timestamps.
Every sync then produces a delta with two independent channels:
{"operation":"create","record":{…}}
{"operation":"update","record":{…}}
{"operation":"delete","record":{…}}
whole records, keyed by record id —
a database changelog, replayed in order
changes.raw = {
changed: ["raw/openclaw/MEMORY.md"],
deleted: []
}
changed files carried whole, byte-exact —
restore on the same runtime prefers these
Channel 2 makes workspace restore always faithful, no matter how a file was edited. Channel 1 is what powers the dashboard, cross-runtime restore, and history — and its quality depends entirely on record identity being stable. That's what reconciliation guarantees: the same memory keeps the same id across syncs, even when the agent rewrites its store in place.
Memory lives in files, so the obvious design is the one git perfected: diff the text, ship line patches, replay them. ALF deliberately doesn't do that, because syncing an agent's memory has different requirements than versioning its source code. The unit that matters is not a line — it's a memory: a thing with a type, a birth date, a provenance, and a lifecycle. The whole design follows from choosing that as the unit of change.
| git-style patch | ALF record delta | |
|---|---|---|
| Unit of change | line hunks inside a file | a whole memory record (plus whole raw files) |
| Applies by | context matching — fuzzes or conflicts when the base drifts | record id — update replaces, delete removes; replay is deterministic |
| Identity | none stored; renames/moves are inferred at read time, heuristically | resolved once at write time (reconcile) and carried on the wire |
| Meaning | every consumer must re-parse files to find "memories" | records index directly: type, timestamps, provenance, status |
| Database stores | binary SQLite doesn't line-diff | a row simply is a record — same wire format as markdown |
| Cross-runtime restore | a MEMORY.md patch is meaningless to a SQLite store | records re-project into any runtime's native shape |
| Conflicts | merge markers; a human resolves | single writer + sequence check; a stale writer gets a clean 409 → restore |
| Server cost | parse, apply, merge server-side | opaque blob + one atomic sequence compare — the server never opens a delta |
| Erase one memory | history rewrite (filter-branch) — breaks every clone | surgical purge by record id, replacing only affected partitions |
The sync stream doesn't just feed restore — it feeds the dashboard, search indexing, and (eventually) lineage. Those consumers ask questions like "which memories changed this week?" and "when did this fact first appear?". A record changelog answers them by construction; a patch stream would force every consumer to re-parse workspaces and re-derive meaning. It also keeps the backend honest and cheap: ingest is a blob write plus one atomic sequence check. All semantics live client-side, in the records.
A line patch only applies against the exact text it was cut from; under drift it fuzzes, and under conflict it stops and asks a human. There is no human here — syncs run from shutdown hooks and cron on unattended runtimes. Record operations don't have that failure mode: an update replaces record 6f3e wherever and whatever it currently is, and concurrent writers are excluded up front by the sequence check rather than merged after the fact. Nothing in the pipeline can ever emit a conflict marker into an agent's memory.
Git stores snapshots and infers what moved or renamed later, per diff, per tool, heuristically. ALF does the inference exactly once — in the reconcile pass, against the last-synced base, deterministically — and then ships the conclusion. Every consumer downstream inherits stable identity for free. That single decision is what lets the dashboard show one memory evolving across months instead of a parade of unrelated records.
ZeroClaw's memory is a SQLite database; Hermes' primary memory is one too. Line diffs don't exist for them. Records give markdown stores and database stores the same unit of change and the same wire format — a section behaves like a row. One mechanism, three very different frameworks, no adapter-specific diff logic anywhere.
The raw channel carries changed files whole, byte-exact — no hunks, no patch application, no risk of a mis-applied edit corrupting a workspace. At memory-file sizes (kilobytes), the bandwidth a line diff would save is noise; deltas are already small because they carry only what changed, and an idle sync uploads nothing at all. The efficiency that matters — not re-uploading unchanged memories — comes from stable identity, not from hunk granularity.
alf sync actually does
Three properties fall out of this loop. Idle syncs are free — if reconcile finds every
record unchanged and no raw bytes differ, nothing is uploaded (no_changes: true).
The cloud is an append-only ledger — a delta blob, once written at its sequence, is never
rewritten. And identity survives curation — the reconcile step is where an in-place edit
becomes a clean update to the same record instead of identity churn.
OpenClaw's MEMORY.md is not a log — the agent curates it: rewrites sections,
re-ranks them, drops what stopped mattering. Here is a workspace at the last sync
(sequence 2), with the four records ALF extracted from it:
# MEMORY.md ## Identity → record 6f3e · created 2026-05-14 Name: Atlas · Human: Johan · Base: Cape Town Reference code: ATLAS-SEM1-7F3A ## Preferences → record 9b12 · created 2026-05-14 Terse answers. Metric units. Tide times in SAST. ## Projects → record c481 · created 2026-06-02 Reef-camera build; tide-log automation. ## Scratch notes → record 2ea7 · created 2026-06-28 Try the new tide API next week.
One real agent turn later, the file has been rewritten wholesale: sections re-ranked, the reference code replaced in its slot, a lesson added, the scratch note gone.
# MEMORY.md ## Preferences moved up — text identical Terse answers. Metric units. Tide times in SAST. ## Identity body edited under the same heading Name: Atlas · Human: Johan · Base: Cape Town Reference code: ATLAS-SEM2-9E4C ## Projects moved — text identical Reef-camera build; tide-log automation. ## Lessons learned new section WorldTides v3 rate-limits at 60 req/min — batch nightly. ## Scratch notes — removed by the agent
At sync time, ALF pairs the fresh export against the base — by identical content first,
then by heading — and carries each matched record's id and original
created_at forward:
The uploaded delta says exactly what happened, and nothing else:
{"operation":"update","record":{"id":"6f3e…","content":"## Identity\n… ATLAS-SEM2-9E4C",
"temporal":{"created_at":"2026-05-14T09:12Z","updated_at":"2026-07-04T15:54Z"}}}
{"operation":"create","record":{"id":"57d0…","content":"## Lessons learned\nWorldTides v3 rate-limits …",
"temporal":{"created_at":"2026-07-04T15:54Z"}}}
{"operation":"delete","record":{"id":"2ea7…","content":"## Scratch notes\n…"}}
+ raw overlay: changes.raw.changed = ["raw/openclaw/MEMORY.md"] ← the whole file, byte-exact
alf restore --at-sequence 2 rebuilds the workspace exactly as it was before the
curation. Nothing in the ledger is ever rewritten.
ZeroClaw keeps all agents' memory in one SQLite database, brain.db, one row per
memory with a native UUID primary key. ALF exports one agent's slice
(WHERE agent_id = ?) and uses the row's own id as the record id. Because identity is
stored with the data — not derived from position — this store never had the curation
problem: reconciliation just passes it through.
| id | key | content | updated_at |
|---|---|---|---|
| 77aa… | project.reef-camera | Reef camera build — housing t.b.d. | 2026-06-02 |
| b2c8… | tides.api | Use WorldTides v3 | 2026-06-10 |
| 44fe… | scratch.todo | Try the new tide API next week | 2026-06-28 |
The agent's framework then mutates its memory the normal SQL way:
UPDATE memories SET content = 'Use WorldTides v3 — 60 req/min cap' WHERE id = 'b2c8…'; INSERT INTO memories (id, agent_id, key, content, …) VALUES ('e91b…', '8423…', 'lesson.rate-limits', 'Batch tide pulls nightly', …); DELETE FROM memories WHERE id = '44fe…';
The next alf sync maps those one-to-one — same native id, same delta op.
Unchanged rows match by id + content (pass P0); the edited row keeps its id through the
id-equality pass (P3):
{"operation":"update","record":{"id":"b2c8…","content":"Use WorldTides v3 — 60 req/min cap", …}}
{"operation":"create","record":{"id":"e91b…","content":"Batch tide pulls nightly", …}}
{"operation":"delete","record":{"id":"44fe…", …}}
other agents' rows in the shared brain.db: untouched, never exported into this agent's archive
Hermes is often lumped in with OpenClaw, but only half of it behaves that way. Its
primary memory is a session log in state.db: one record per conversation, keyed
by the session's native id. A conversation is never rewritten — a later chat is a
new session — so this store is structurally immune to the curation problem and always was.
already synced: 20260702_183045_7b21 "Chose acrylic for the camera housing" new since last sync: 20260704_143011_9c4e "Debugged tide-log cron; fixed TZ"
Record id = UUID derived from the immutable session id. Delta: create — exactly one, never an update.
## Reef camera Housing: v1 3D-printed shell. Housing: v2 acrylic (v1 leaked). edited in place, heading unchanged
Same shape as OpenClaw's MEMORY.md — reconcile pairs by heading (P2), carries the id. Delta: update — one record, same identity.
The lesson generalizes: stores that append with native keys never needed help (Hermes sessions, ZeroClaw rows); stores that are curated in place get their identity from reconciliation (OpenClaw's MEMORY.md, Hermes' notes layer). One mechanism covers all of them — the adapters don't implement any diffing themselves.
The service stores the first sync as a full snapshot at sequence 0 and every later sync as a delta blob at the next sequence. Restore fetches the snapshot plus the deltas after it and replays them; point-in-time restore replays only up to the sequence you ask for.
Because reconciliation keeps ids stable, this ledger is also readable: the dashboard can show record 6f3e as one memory whose content evolved at sequence 3 — not as a parade of unrelated records winking in and out of existence. And because deltas are never rewritten, the curation at sequence 3 destroyed nothing: the pre-curation state is permanently addressable at sequence 2.
Reconciliation is deterministic and threshold-free — five passes, run in order, each consuming the records it matches. Matching is scoped per source file so unrelated files can't cross-pair.
| Pass | Matches on | Result |
|---|---|---|
| P0 | same id and same content (any file) | carried unchanged — protects native-keyed stores |
| P1 | identical content within the same file | carried unchanged — reorders and re-saves cost nothing |
| P2 | same ## heading within the same file | update — id and created_at carried, body replaced |
| P3 | same id (leftovers) | update — native in-place edits (ZeroClaw rows) |
| P4 | no match | create with a content-derived birth id · delete for vanished records |
| You (or your agent) did… | Memory delta | Raw delta |
|---|---|---|
| Nothing (idle re-sync) | nothing — no upload | — |
| Re-saved a file unchanged | nothing | — |
| Reordered / re-ranked sections | nothing | 1 file |
| Edited one section's body | exactly 1 update — same id, original created_at | 1 file |
| Added a section / row / session | exactly 1 create | 1 file |
| Removed a section / row | exactly 1 delete | 1 file |
| Renamed a heading and edited its body | delete + create — correct end state, lineage break | 1 file |
| Moved a section to another file | delete + create | 2 files |
Whatever the structured delta says, the raw channel always carries the changed files byte-exactly — so a same-runtime restore reproduces the workspace precisely, and the structured records can afford to be a clean, semantic history rather than a defensive copy.