Skip to content

Back up a mem-repo to a remote

A mem-repo workspace keeps every mem as a branch in one local git repository that the engine owns. That repository can be pushed to any git remote — a bare repo on a NAS, a private GitHub repository, a USB stick — and recovered from it, using only memstead commands. This guide walks a full backup and recovery cycle.

Transport is deliberately a CLI-only surface: agents connected over MCP have no push/fetch primitive. Moving a mem-repo off-machine is an owner decision, made at the terminal.

The transport commands need the full build (the default memstead binary) and a git-branch-backed workspace (memstead mem-repo init).

Point a named remote at wherever the backup should live. Any URL git accepts works — SSH, HTTPS, or a local path:

Terminal window
# a private GitHub repo…
memstead mem-repo remote-add origin git@github.com:you/mem-backup.git
# …or a bare repository on any disk
git init --bare /backups/mems.git # one-time, outside the workspace
memstead mem-repo remote-add origin /backups/mems.git

remote-add is an upsert: re-running it with a new URL re-points the remote. Transport commands default to origin; pass --remote <name> to use another.

Each mem is one branch; push the ones you want backed up:

Terminal window
memstead push knowledge

The argument is the mem’s name; the pushed ref is the branch the mem’s mount declares. The two usually coincide (knowledge lives on refs/heads/knowledge), but a mount may declare any branch it likes — a mem named engine on refs/heads/team/engine pushes that declared branch, and pull --all restores that declared branch from refs/remotes/origin/team/engine.

The push refuses rather than surprises:

  • UNKNOWN_REMOTE — the remote name isn’t configured (step 1).
  • NON_FAST_FORWARD — the remote has commits this workspace lacks (another machine pushed). Run memstead pull <mem> first, or --force to overwrite deliberately (force-with-lease under the hood).
  • LOCAL_INVALID_STATE — the local branch fails schema validation; nothing is sent. Fix the named entities first.

Recovery is memstead pull --all, the inverse of push --all. It brings the workspace’s schema-and-config ref first, so every schema and mem config the backup carries is in place before any content is checked against it; then every mounted mem’s branch, validated against the schema it now resolves to; then the mounted mems’ check records. It only fast-forwards: a branch missing locally is created, a branch with local commits the remote lacks is refused by name and left alone. A mem that could not load because its schema existed only in the backup serves again afterwards.

The workspace’s engine state came with a clone

Section titled “The workspace’s engine state came with a clone”

The usual case: the workspace lives inside a repository that tracks .memstead/ (the mount roster, the workspace config) and ignores mem-repo/, so a clone on the second machine has the roster but no mems. Two commands restore it:

Terminal window
memstead mem-repo init --remote git@github.com:you/mem-backup.git
memstead pull --all

--remote configures origin as part of the bootstrap. The output lists every ref that moved; memstead status --remote then reports everything in sync.

When only the backup exists, the roster has to be rebuilt, one mount per mem. pull --all names each mem the backup carries together with the exact command that mounts it:

Terminal window
mkdir restored && cd restored
memstead mem-repo init --remote git@github.com:you/mem-backup.git
memstead pull --all # schemas and mem configs arrive; each unmounted mem is named
memstead workspace allow-create --schema '*' knowledge
memstead mem init knowledge --schema widgets@0.1.0 --reattach
memstead pull --all # the mem's branch and its check records
memstead status # your entities are back

--reattach mounts the mem against the config the backup already holds instead of writing a fresh one, so the schema-and-config ref keeps the backup’s history and the next push --all from this machine fast-forwards. The pin to pass is the one pull --all printed for the mem.

If the mem was published as an archive instead, memstead install is the simpler recovery; this flow is for live workspaces with history.

Once the histories are shared, the ongoing cycle is plain fetch-inspect-pull:

Terminal window
memstead fetch knowledge # see what moved (updated refs print per ref)
memstead pull knowledge # fast-forward + reload, refuses on divergence
memstead pull --all # the schema ref, every mem branch and the check records
memstead push knowledge # send your own commits
memstead push --all # every mem branch plus __MEMSTEAD and __MEMSTEAD_CHECKS, fast-forward only

A mem’s check records travel with it. The check ledger lives in the workspace, not on the mem’s branch (a check is never a commit, which is what keeps staleness derivable by hash), so the mem-repo carries one more engine-owned ref, __MEMSTEAD_CHECKS, with the mem’s ledger rows as last published. push publishes the mem’s rows there and pushes the ref beside the branch; fetch and pull fetch it and add the mem’s rows to the local ledger. A row is a fact whose identity is its bytes: two machines checking the same mem merge by union, nothing is rewritten, a round trip adds nothing, and an export made on the other machine seals the checks recorded here. Only the rows of a mem this workspace mounts are ever imported, and only through its own fetch, pull and push; a fork’s ref is never read, so a contributor cannot ship check rows that close your gates.

push --all is the whole-workspace form: it lists the remote once, skips every ref already in sync (a run with nothing to send prints nothing), prints one line per ref it moved, and names a ref it could not fast-forward (NON_FAST_FORWARD) while still pushing the others, exiting non-zero at the end. It also carries the __MEMSTEAD ref, where schemas and mem configs live, which the single-mem form has no route for. There is no --force on --all. pull --all is its inverse and the one route that moves the local __MEMSTEAD ref from a remote: a fetch given a refspec that writes into a local branch is refused, since a fetch only updates remote-tracking refs.

pull validates the incoming tree against the mem’s pinned schema before moving anything (SCHEMA_VIOLATION_IN_FETCH refuses the whole pull), fast-forwards the branch, and reloads the in-memory store — the graph reflects the pulled state immediately.

Divergence (both sides committed) never merges silently: pull refuses with LOCAL_DIVERGENCE, and you choose — branch-reset to adopt the remote, or push from the other machine first.

A proposal against a mem is a second lineage, not an unrelated mem: fork it, and the diff between the two later has a common ancestor to stand on.

Terminal window
memstead mem fork knowledge knowledge-proposal # at the source's tip
memstead mem fork knowledge@<sha> knowledge-proposal # at a commit the source reaches
memstead mem fork knowledge knowledge-proposal --remote origin # source fetched from the remote
memstead mem fork knowledge knowledge-proposal --remote origin --adopt # the fork branch already on the remote, mounted as a fork of the local source

The fork’s branch starts at the source’s commit and makes one commit of its own right above it, the fork commit: the anchors and derivations sidecar ids and every body link the source qualified with its own name ([[knowledge--slug]], [[knowledge:slug]]) now carry the fork’s name, with hashes, spans and observations unchanged and links naming any other mem untouched. Its config is the source’s (same schema pin, same version and description) with forkedFrom written in: source mem, sha (the ancestor on the source), remote when one was used, and base (the fork commit), the commit a later comparison against the source stands on. The entities read as the source’s at that commit, under the fork’s own name. A local fork carries the source’s outgoing cross-link grants under its own name, so copied cross-mem edges read conformant from the first read; it starts with no check records (a fork’s entity is not the same entity as its source’s). mem list shows the origin (forked from ..., with the base). The name obeys the same create rules as mem init, and every refusal (a sha not on the source branch, a name a branch sits above or below, an existing name, a folder or archive source) lands nothing.

With --remote, the source branch and the remote’s __MEMSTEAD config are fetched into remote-tracking refs (the local __MEMSTEAD never moves), the fetched tree is validated as pull validates before the mount exists, no grant is inherited (the remote’s policy is not this workspace’s), and a schema pin this workspace cannot resolve refuses naming the remedy, memstead schema install.