How the ALF watch loop works: token-free backup from inside an MCP session
Agent Life 1.1 added a watch loop to the alf MCP server: while your agent’s session is alive, its memory is backed up automatically — event-driven, debounced, and at zero token cost. This post walks through the machinery: what gets watched, how a file change becomes a cloud delta, why torn writes never sync, and what happens when things go wrong. It assumes you know roughly what Agent Life does (portable backup and sync for AI agents) but not how.
The problem it replaces
Before 1.1, hands-off backup meant one of two fragile things. A cron job wrapping alf sync — which breaks silently when the schedule, the machine, or the workspace path drifts, and syncs on a timer regardless of whether anything changed. Or prompting the agent to sync — which works, but spends model context on remembering to do chores, every session, forever.
The watch loop replaces both with a mechanism that costs nothing at rest: it runs inside the alf mcp serve process your agent’s framework already spawns, reacts to filesystem events, and syncs through exactly the same pipeline a manual alf sync would. No model involvement, no tokens, no crontab.
Where it lives — and deliberately doesn’t
The loop is part of the MCP server, so it exists exactly as long as an MCP session does. There is no daemon mode — that’s a deliberate choice, not a gap. A resident background service would need its own installer, supervisor, upgrade path, and security surface on every machine. Instead, the loop’s lifetime is tied to the thing it protects: while your agent runs, its backup runs. For a headless box where no session is ever alive, the classic cron-driven alf sync remains the right tool.
One server serves one agent. The runtime and workspace are pinned when the host spawns the server, so a machine hosting several agents runs one (cheap) server process per agent, each watching only its own surfaces.
What it watches
Each framework adapter declares its own watch surface — the same code that knows how to export a runtime’s memory knows what to watch:
- OpenClaw: the workspace tree (OpenClaw agents scatter Markdown memory into agent-chosen folders, so the whole workspace is one recursive source), plus the install’s config file.
- ZeroClaw: the shared
brain.dbSQLite store with its WAL and SHM sidecars, the markdown memory directories, the root identity files, and the AIEOSidentity.json. - Hermes: the profile’s allowlisted files, the
state.dbsession store (again as a three-file trio), skills — and the profiles directory itself, so a new agent created mid-session shows up inalf_agents_listwithout a restart. - Generic: whatever the workspace’s
.alf-map.jsondeclares, per source.
On top of the adapter surface, the loop watches the agent’s encrypted credential vault (so alf vault add from another terminal rides the next auto-sync) and the sync-control files themselves (the tracked-file include list and sync log). Sources that don’t exist yet — a memory directory the agent hasn’t created — are tracked from their nearest existing ancestor and picked up the moment they appear.
Watching is event-driven via the OS’s file-notification API, with a periodic rescan as a safety net. If watcher registration fails outright (some containers and network filesystems), the loop degrades to polling that same rescan — coverage is never silently narrower than the declared surface.
From file change to cloud delta
A filesystem event doesn’t trigger a sync; it marks a source dirty. The actual sync decision passes through three gates:
- The interval gate. Each channel has a cadence: ordinary memory/content changes ride the delta channel (default every 15 minutes, floor 1 minute), while tracked-file changes — which force a full snapshot rollover rather than a delta — get a deliberately slower channel (default 1 hour, floor 15 minutes). Everything is clamped to a 24-hour ceiling. Your agent tunes these in-session with the
alf_watch_settool, or statically in the generic map’swatchblock. - The quiesce gate. A dirty source must have been still — no further events — for a settle window (3 seconds) before it’s captured. A file being actively written never syncs mid-write.
- Single-flight. One sync at a time per agent. A tick that fires while a sync is running coalesces into the next one; failures back off instead of hammering.
When the gates open, the loop captures the changed sources and hands them to the exact same delta pipeline the CLI uses: compute what changed against the last-synced base snapshot, upload a compact delta (or a fresh snapshot when a tracked file forces a rollover), advance the sequence. If you want that half of the story — snapshots vs. deltas, content-addressed memory records, how restore replays history — it’s written up separately in how delta sync works.
On startup, the loop runs a catch-up scan that marks every source dirty once — so changes made while no session was alive are swept into the first tick instead of waiting for the next edit.
Why it never syncs garbage
A backup that occasionally uploads a torn write is worse than no automation, so the loop’s refusals matter as much as its actions:
- No torn bytes. The quiesce gate applies to every source with no exceptions — including live SQLite databases. A
.dband its-wal/-shmsidecars are captured together as one byte-preserving unit after the settle window: near-consistent, and honestly documented as such. (There is a transactional-snapshot primitive — SQLite’sVACUUM INTO— in the codebase, but it’s deliberately reserved for a future row-extraction mode: it defragments the database, which would break the byte-for-byte fidelity the raw backup model guarantees.) - Loud, not silent, when a source never settles. A store that’s written so continuously it never passes the quiesce gate for 24 hours raises a warning in
alf_statusinstead of quietly never syncing. - Secrets can’t ride along. The sensitive-path denylist (
.env, key files, SSH identities) is enforced at tracking time and re-validated at export — a denylisted file in the include list is skipped with a warning, never packed.
Crash-safety and coexistence
The loop is designed to be killed. Local sync state is written atomically (write-new, fsync, rename), and the service enforces a compare-and-set on every upload’s sequence number — so a SIGKILL mid-sync leaves either the old state or the new state, never a half-advanced cursor, and a stale process can’t overwrite newer history.
It also shares the machine gracefully. The CLI, the MCP tools, and the watch loop all take the same per-agent advisory lock, so a manual alf sync from a terminal and a background auto-sync can never interleave: whoever arrives second waits briefly, then reports agent_busy rather than guessing. Current CLI users keep their exact workflow — the loop is purely additive.
When it can’t fix things: recover once, then park
Some situations shouldn’t be retried forever: the local base snapshot was deleted, another machine synced the same agent first (a sequence conflict), or the base is unreadable. For these, the loop attempts recovery once — re-pulling the authoritative base from the cloud — and if that doesn’t resolve it, it parks: auto-sync for that agent stops, and alf_status reports exactly why. The agent (or you) resolves the situation deliberately, instead of a background process making destructive guesses about whose history wins. That philosophy — automation handles the routine, ambiguity escalates to someone accountable — is the same reason destructive operations like alf purge aren’t MCP tools at all.
Deferred sync: the tracked-file pause
In 1.1, while a tracked-file change is waiting out its (slower) interval, the whole agent’s auto-sync waits with it — and because the startup catch-up scan marks everything dirty, tracking a new file effectively starts that clock at session start. Nothing is lost: the deferred sync captures every change when it fires, and a manual alf sync is unaffected. If the pause matters, lower tracked_files_interval via alf_watch_set. Letting untracked sources ride the fast channel while a tracked source waits is queued for 1.2.
Try it
Point your MCP-capable framework at the server and the loop comes with it:
alf mcp serve -r openclaw -w ~/.openclaw/workspace
- Host configuration (Claude Code, Hermes, ZeroClaw): agent-life.ai/cli#mcp-client-configuration
- The watch loop and full tool reference: agent-life.ai/cli#alf-mcp-serve
- The delta pipeline it feeds: agent-life.ai/delta-sync.html
Agent Life is open source, MIT licensed — portable backup, sync, and migration for AI agents.
Halimede builds agent-life. Get new posts by email.
Previous post: Agent Life 1.1: ALF now works with any agent framework →