freedzhost.cloud

An agent's state as a portable file: moving a running agent between machines

Level AdvancedEstimated time 2 hRead first Tailscale as the backbone: everything private, reachable from anywhereA word you don't know? Glossary

Most agent tools keep a conversation in a database row. That makes three things painful: moving it to another machine, changing its model halfway, and trusting it after a crash. All three get easier when the state is a folder of plain files that anyone can open.

What you get. A capsule format you can inspect with tar, a handover that re-renders the same history for any provider, a move protocol that cannot run an agent twice, and a way to test the failure cases instead of hoping.

1. The capsule: state you can read

A capsule is a directory (or the tar of one):

manifest.json                    identity, holder, a digest for every other file
memory/working_memory.md         the agent's own notes
memory/timeline_events.jsonl     the journal: one immutable event per line
memory/vector_index/index.json   local semantic index
artifacts/snapshot.json          the workspace as path to content hash
artifacts/blobs/<sha256>        file bodies, stored once by content
environment/spec.json            how to rebuild the sandbox

2. Switching model mid-conversation

The journal records what happened (a user message, a tool call, a result), not how one vendor spells it. Rendering for a given provider is a pure function of the journal, so you can switch and switch back with no loss. Three rules make it work:

3. Moving a running agent

The invariant: at most one machine runs the agent, ever. The protocol is a two-phase commit with a fencing epoch.

freeze on the source  ->  offer(epoch+1) to the target
send only the blobs the target lacks  ->  target verifies every digest
target prepares (renders the journal, builds the sandbox)
--- point of no return ---
activate(epoch+1) on the target  ->  source marks itself retired

Any failure before the point of no return rolls back: the target discards its staging area and the source resumes at its old epoch, as if nothing had happened. A refused offer, a corrupted chunk and a dead link are all handled the same way.

4. The one ambiguous moment

If the reply to activate is lost, the source cannot tell whether the target took over. Guessing either way risks two copies. Instead, ask the target which epoch it holds:

A machine that was partitioned and wakes up with an old epoch can offer a move, but the target refuses any epoch that is not newer than the one it holds. An epoch is compared, never waited on, which is why it is safer than a lock with a timeout.

5. Test the failures, do not read the code

Wrap the target in a test double that fails one named call, either before it runs (the request was lost) or after (the reply was lost). Then drive the real code through every phase and check the invariant: one holder, an empty staging area, a capsule that still verifies. Add a corrupted chunk, a stale epoch and a crash that loses the holder record. Writing these tests found a real bug in our first version: the source and the target keyed the agent by different identifiers, so the target never recognised the epoch.

Traps

Write every file atomically (temporary file, then rename). A crash must leave a file complete or absent, never half written.

A signature over a capsule attests an exported state. A capsule that keeps running changes its holder and journal, so sign again at export, not at rest.

Authenticating a link is not encrypting it. Run the transport over a private network such as your tailnet, or a TLS tunnel.

Source, MIT: git.freedzhost.cloud/bengua/inari-core. Related: Inari, self-hosted and the glossary.

Found this useful? Tell us what to measure next.