Portable Agent Sessions
Why moving an agent between servers loses its history today, and a proposal for signed, content-addressed session logs that any server can verify and sync.

The problem: sessions are trapped on the server that made them

The Agents desktop app can move an agent from one agent server to another. Because agent servers never talk to each other, the app itself performs the move: it reads the agent's portable state from the source server using ordinary signed actions (The Signed Protocol), recreates it on the target, and only then deletes the original.

Today's move: definition, memory, tools and triggers copy; sessions and keys cannot

That works for the agent's definition, memory files, authored tools, and triggers. It does not work for sessions. The target server has no import API for durable session events, and even if it did, it would have no reason to believe that whatever the desktop uploaded is a faithful record of what happened. Sessions and run history are therefore deleted along with the original agent. The dialog says so, but the result is still a surprise: an agent arrives on its new server with its personality intact and its entire memory of past conversations gone.

This is not a bug in the move workflow. It is a consequence of how sessions are stored today: as rows in a SQLite database owned by one server, with no identity, no signature, and no provenance. Every other kind of Seed data — documents, comments, capabilities, contacts — is a signed, content-addressed blob that any peer can verify and re-host. Sessions are the exception, and the move workflow is where that exception becomes visible.

The fix is to stop making them the exception.

Proposal: sessions as signed, hash-linked logs

Model a session the way Seed already models a document's change history: a chain of signed DAG-CBOR blobs in IPFS, where each blob commits to the CID of the one before it.

A session is a hash-linked log of signed messages stored as IPFS blobs

Message types

Each entry in the log is one blob. Four kinds cover everything a transcript needs:

    Session genesis — declares the agent (hm:// id), the owning account, a nonce, and a timestamp. Signed by the user's account key. Its CID is the session's permanent identity.

    User message — prev CID, the message body as blocks, and a list of attachments as ipfs:// links. Signed by the user's account key (or a key the user has delegated, exactly as documents can be).

    Model response — prev CID, the provider/model that produced it, token usage, and the content blocks. Signed by the hosting server's key.

    Tool call and result — prev CID, the tool name and input, and the output. Small outputs inline; large ones are separate UnixFS blobs referenced by CID. Signed by the hosting server's key.

Attachments (images the user pasted, files the agent read or wrote, screenshots from a browser tool) are never embedded. They are their own IPFS blobs, linked by CID from the message that introduced them. Large tool outputs work the same way. This keeps the chain blobs small and lets a verifier skip fetching the payloads it doesn't care about while still being able to check that they are what the message claimed.

Why a chain

Because every message carries prev, the CID of the head message identifies the entire history. Two servers that agree on the head agree on every byte of every message before it. A server that is handed a head CID and a bag of blobs can reconstruct the session deterministically, and any missing or tampered blob fails to hash. There is no "merge": like a document's change DAG, the log has one writer per entry and a strict ordering, so the hard parts of distributed state simply don't arise.

This is the same trick a blockchain uses, minus the consensus. Nobody is competing to append; the question is only "did the people who are allowed to write this session actually write these entries, in this order?" — and the signatures plus the hash links answer it.

What this fixes: syncing sessions between servers

With signed logs, moving a session is no longer an import of untrusted rows. It is fetching blobs and verifying them.

Syncing a session is fetching blobs and verifying signatures and chain links

A target server accepting a session checks:

    Every blob hashes to its CID.

    The prev links form one unbroken chain back to a genesis.

    Every signature verifies against its claimed signer.

    Every server-signed entry was signed by a server key that the agent's owner had authorized at that point in the log (see Authorizing servers below).

    The head CID it was told about is actually the tip of that chain.

If all five hold, the target server has exactly as much assurance about the history as the source server had. It does not need to trust the source server, the desktop that relayed the blobs, or an IPFS gateway in the middle — the data proves itself. The new server can then continue the chain: its next model response has prev pointing at the old head, signed with its key.

The source does not even need to be online. The user's desktop can publish the session head as a signed Ref on the user's account (the same mechanism that points a document path at its latest version), and any server the user authorizes can discover the head and pull blobs from whatever peer has them. That is also what makes the move workflow's "copy, then delete" safe to turn into "copy, verify, then delete" — the target can prove it has the complete history before the source lets go of anything.

What this enables: edits that carry their context

Agents publish changes to Seed documents. Today a change blob records who made it (the agent's publishing identity) but not why. If a session is a signed log with stable CIDs, a change can cite the exact message that caused it.

A document change blob links back to the tool-call message that produced it

The document Change blob gains a context field holding the CID of the tool-call message that produced it. That CID is inside a chain, so from it a reader can walk backwards to the model's reasoning, the user's instruction, and any attachments that were in play — all signed, all content-addressed, none of it editable after the fact.

Concretely, when reviewing a document's history, a change authored by an agent becomes inspectable: open the change, follow the context link, read the prompt that led to it. If the session lived on a server that has since disappeared, the blobs may still be pinned by the user's desktop or any peer that replicated the document, because they are ordinary IPFS blobs like everything else the document references. The Agents roadmap already lists "agent data signed and saved to IPFS, referenced from docs when writing"; this is the structure that makes the reference meaningful.

Streaming stays stateful; commits are signed

None of this requires model output to be signed token-by-token. Streaming is an experience, not a record.

Streaming flows over an ephemeral WebSocket; at the end of each turn the server signs and stores the durable messages

The server keeps its role as the stateful process that runs the model loop, holds the provider connection, and pushes deltas to connected clients over the signed WebSocket. What changes is what counts as the source of truth:

    When the user's message arrives (already signed by the user), the server stores it as a blob and pins it.

    As the model streams, deltas go to clients as they do now. Nothing about them is durable.

    When a tool call completes, the server signs a tool-call blob with prev = the current head and stores it.

    When the response completes, the server signs the response blob and stores it. That CID is the new head, and the "turn complete" event tells clients what it is.

If the server crashes mid-turn, the chain simply ends at the last committed blob. Partial deltas are lost, which is what happens today too — but the durable record never contains anything half-written or unsigned. Clients that reconnect can compare the head they last saw with the head the server reports and fetch only the blobs between them.

Trustless agents in public

The deeper reason to do this is that it makes agent activity auditable by people who don't run the server.

Today, when an agent comments on a public document, the comment is signed by the agent's publishing key and anyone can verify that. But nobody outside the server can see what prompted it, which model produced it, or whether a tool result was real. With signed session logs, an agent can choose to make a session public, and then:

    Anyone can verify the transcript was produced by the server key the agent's owner authorized.

    Anyone can verify that a comment or edit really came from the tool call the log says it did.

    Two agents on different servers can interact through documents and comments, and each can inspect the other's public sessions to see the reasoning behind a change before accepting or building on it.

    A third party can re-run the same prompt against the same model and compare — the log records exactly which model and what input.

This is the building block for agents that collaborate in public without a shared operator. The server's signature is its accountability: a server that fabricates tool results or rewrites history produces blobs that don't match what other servers and clients have already seen, and its key can be deauthorized.

Authorizing servers

A server signs with its own keypair, not the user's. For a verifier to accept a server-signed entry, the agent's owner must have delegated that right. Seed already has the primitive for this: capability blobs. An owner issues a Capability granting a server's key the agent-session-writer role for a specific agent, and revokes it by issuing a tombstone. A verifier checking step 4 above looks up whether a valid capability existed for that server key, on that agent, at the time the entry claims to have been written.

This is what makes "move agent" clean in the end state: the owner authorizes the new server, the new server pulls the log and continues it, the owner revokes the old server. The old server's past entries remain valid — they were written while it was authorized — and any entry it tries to append afterwards fails verification everywhere.

Rollout sketch


    Define the blob schemas. Session genesis, user message, model response, and tool call, in the same SDK pattern the client library uses for comments and capabilities: build unsigned, CBOR-encode with a zeroed signature, sign, fill, encode. Add the context field to the document Change schema.

    Give servers a key and a capability flow. Servers already hold account keys for publishing; add a server identity key and a UI step in agent settings that issues the session-writer capability when an account starts using a server.

    Dual-write. Keep the SQLite event log for the current UI and, on each committed turn, also write the signed blobs. Store the head CID on the session row. Verify the two agree in tests.

    Read path. Build the transcript view from blobs, using SQLite only as an index. At this point the session's truth lives in IPFS.

    Sync. Add ImportSession {headCid} (or make the existing session list accept a head ref) which fetches, verifies, and adopts a chain. Replace the move dialog's "sessions are deleted" warning with "sessions are moved."

    Provenance. Have the write tools populate context on every Change they publish, and render the link in document history.

    Public sessions. Let an owner mark a session public, which publishes its head as a Ref under the agent's account so anyone can fetch and verify it.

Steps 1–4 are internal and reversible. Step 5 is what fixes the move workflow. Steps 6–7 are where the trustless story pays off.

Open questions

    Private sessions and encryption. Blobs in IPFS are readable by whoever has the CID. Seed has no public-key encryption layer today; private sessions would initially rely on not announcing the CID, with a real encryption design as follow-up work.

    Blob size and retention. Long sessions with many tool outputs accumulate data. Linking large outputs by CID rather than inlining them is the main mitigation; servers should also be free to garbage-collect payload blobs whose sessions the owner has deleted, while keeping the chain blobs small enough to retain indefinitely.

    Model nondeterminism. A signed log proves what a model output, not that the output was the only possible one. That is fine for accountability and weaker than it sounds for reproducibility; the document should not over-promise the latter.

    Sub-sessions and parallel tool calls. The chain is linear. Sub-agent sessions get their own chain with a genesis that cites the parent message; parallel tool calls within one turn are committed in the order they complete, which is an ordering choice rather than a constraint.

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime