Agent Life Format · Sync internals

How ALF syncs agent memory

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).

Mental model

A delta is a changelog of records, not a text patch

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:

Channel 1 · structuredmemory/delta.jsonl
{"operation":"create","record":{…}}
{"operation":"update","record":{…}}
{"operation":"delete","record":{…}}
whole records, keyed by record id —
a database changelog, replayed in order
Channel 2 · raw filesraw/openclaw/MEMORY.md
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.

Design rationale

Why record deltas, not git-style patches

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 patchALF record delta
Unit of changeline hunks inside a filea whole memory record (plus whole raw files)
Applies bycontext matching — fuzzes or conflicts when the base driftsrecord id — update replaces, delete removes; replay is deterministic
Identitynone stored; renames/moves are inferred at read time, heuristicallyresolved once at write time (reconcile) and carried on the wire
Meaningevery consumer must re-parse files to find "memories"records index directly: type, timestamps, provenance, status
Database storesbinary SQLite doesn't line-diffa row simply is a record — same wire format as markdown
Cross-runtime restorea MEMORY.md patch is meaningless to a SQLite storerecords re-project into any runtime's native shape
Conflictsmerge markers; a human resolvessingle writer + sequence check; a stale writer gets a clean 409 → restore
Server costparse, apply, merge server-sideopaque blob + one atomic sequence compare — the server never opens a delta
Erase one memoryhistory rewrite (filter-branch) — breaks every clonesurgical purge by record id, replacing only affected partitions

Patches describe text. The consumers need memories.

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.

Patches assume a cooperating base. Agents don't cooperate.

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.

Identity is resolved once, at write time.

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.

Half the fleet's memory isn't text.

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.

And where text fidelity does matter, ALF doesn't diff at all.

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.

"Couldn't each agent just be a git repo?" — For workspace files alone, it could. But you'd still need everything else: a semantic index (parse every commit server-side), per-layer encryption for the credential vault, sequence-addressable point-in-time restore, surgical erasure of a single memory without rewriting history, and database-store diffing — none of which git gives you. ALF keeps git's two best ideas — content-derived identity and an append-only history — and rebuilds them at the granularity the problem actually has: the memory record.
The local lifecycle

What one alf sync actually does

Local machine
Framework storeMEMORY.md · brain.db · state.db
Full exportwhole workspace → records
Reconcilematch vs last-synced base
carry ids + created_at
Compute deltacreate / update / delete
+ changed raw files
POST /agents/{id}/deltas · the service assigns the next sequence number
ALF cloud
S₀ snapshotfirst sync, seq 0
Δ seq 1immutable blob
Δ seq 2immutable blob
Δ seq 3this sync
↺ the reconciled export replaces ~/.alf/state/{agent}-snapshot.alf — the base the next sync diffs against
↑ restore runs the ledger in reverse: fetch snapshot + deltas → rebuild the archive → materialize the workspace

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.

Example · OpenClaw

A curated MEMORY.md, edited in place

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:

last-synced base · seq 2MEMORY.md
# 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.

live workspace · after curationMEMORY.md
# 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

Reconcile matches new sections to old records

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:

Record in base (seq 2)
Matched by
Section in fresh export
Delta emits
9b12 ## Preferences
Terse answers. Metric units…
P1 exact
9b12 ## Preferences
identical text, new position
nothing
6f3e ## Identity
…code: ATLAS-SEM1-7F3A
P2 heading
6f3e ## Identity
…code: ATLAS-SEM2-9E4C
update
c481 ## Projects
Reef-camera build…
P1 exact
c481 ## Projects
identical text, new position
nothing
no match in base
P4 new
57d0 ## Lessons learned
birth id = hash of content
create
2ea7 ## Scratch notes
Try the new tide API…
P4 gone
absent from fresh export
delete

The uploaded delta says exactly what happened, and nothing else:

uploaded at seq 3memory/delta.jsonl
{"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
Where did ATLAS-SEM1-7F3A go? It's gone from the live store because the agent chose to overwrite it — and it is still one command away: alf restore --at-sequence 2 rebuilds the workspace exactly as it was before the curation. Nothing in the ledger is ever rewritten.
Example · ZeroClaw

Rows in brain.db — identity stored with the data

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.

last-synced base · agent 8423's slicebrain.db · memories
idkeycontentupdated_at
77aa…project.reef-cameraReef camera build — housing t.b.d.2026-06-02
b2c8…tides.apiUse WorldTides v32026-06-10
44fe…scratch.todoTry the new tide API next week2026-06-28

The agent's framework then mutates its memory the normal SQL way:

what ZeroClaw did between syncsbrain.db
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):

uploaded deltamemory/delta.jsonl
{"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
Example · Hermes

A hybrid: an append-only log plus a curated layer

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.

append-only · sessionsstate.db
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.

curated · notes layermemories/projects.md
## 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 backend

An append-only ledger you can rewind

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.

alf restore · head = S₀ + Δ1 + Δ2 + Δ3
S₀ · snapshot2026-05-14
2 records
Δ · seq 12026-06-02
create ×1
Δ · seq 22026-06-28
create ×1
Δ · seq 32026-07-04
upd ×1cre ×1del ×1
alf restore --at-sequence 2 · ATLAS-SEM1-7F3A still here

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.

Reference

The reconcile passes, and what each edit costs

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.

PassMatches onResult
P0same id and same content (any file)carried unchanged — protects native-keyed stores
P1identical content within the same filecarried unchanged — reorders and re-saves cost nothing
P2same ## heading within the same fileupdate — id and created_at carried, body replaced
P3same id (leftovers)update — native in-place edits (ZeroClaw rows)
P4no matchcreate with a content-derived birth id · delete for vanished records
You (or your agent) did…Memory deltaRaw delta
Nothing (idle re-sync)nothing — no upload
Re-saved a file unchangednothing
Reordered / re-ranked sectionsnothing1 file
Edited one section's bodyexactly 1 update — same id, original created_at1 file
Added a section / row / sessionexactly 1 create1 file
Removed a section / rowexactly 1 delete1 file
Renamed a heading and edited its bodydelete + create — correct end state, lineage break1 file
Moved a section to another filedelete + create2 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.