Skip to content

Topology, not hierarchy: the four shapes

Two axes decide how you deploy neosian: who runs neosian code and where the bytes live. Four shapes fall out, none of them a hierarchy, and the quickstart always begins embedded.

bytes on local files bytes in a database
your app runs neosian embed + FileStore: dev, local tools, single-writer agents embed + PostgresStore: production, multi-worker
a separate process runs neosian the state process (neosian serve) owning a FileStore root the state process over Postgres

A single app embeds the library and talks to its store directly: files for dev/local, Postgres for production. PostgresStore is a driver, not a process: the database is your existing infra, the SQLAlchemy shape. An embedded app needs no proxy in front of its own store.

The state process (neosian serve) is the first-class answer when state is shared across processes, apps, or languages, including one container in a dev compose beside redis and minio, or when a FileStore root needs more than one writer: one process owns the files and every client speaks to it. It adds no capability the library lacks, only reach. docker run is never step one. And nothing is a one-way door: neosian export DIR writes any store (a root, Postgres, the daemon by --url) to a directory that is itself a FileStore root, and neosian import DIR restores it verbatim into any other, one scope or conversation per request over the wire, version history and actors carried as they were.

One token, one volume, one health check:

Terminal window
docker run -d \
-e NEOSIAN_SERVE_TOKEN=change-me \
-p 6367:6367 -v neosian-state:/data \
ghcr.io/mausa-ai/neosian:<X.Y.Z>
curl -fsS http://localhost:6367/health

The image is published per release tag (amd64 and arm64; latest names the newest final release); the shipped Dockerfile builds the same image from a checkout (docker build -t neosian .).

The default command serves a FileStore on the /data volume; set NEOSIAN_POSTGRES_DSN (and override the command, e.g. --schema neosian) for the Postgres backend. Without the container it is one command: NEOSIAN_SERVE_TOKEN=… neosian serve, where no flags serve the home, --root DIR another root. The token is env-only and an unset token refuses to start; TLS terminates at a reverse proxy.

~/.neosian (or $NEOSIAN_HOME) is the store every command and the playground use when no flag names one, and the root a neosian agent reaches through home(). Per project is a scope, not a root, and a machine is registered once: neosian setup --write writes each client’s own config, which names the home and no mount, and every session derives its layout where it runs (user:<login> at /user, user:<login>/proj:<slug> at /project); project_scope() spells the same for a Conversation. No project carries a file, and a new project needs nothing. One neosian audit inside a project then lists every agent’s work in it. (--level project still writes one directory’s own files: neosian docs agents.)

Many projects and agents on one home is the multi-writer shape, so the answer is the state process on the home: one process owning the files, every hook and MCP server registered with --url. As a per-user service, a docs recipe, never library scope:

<!-- macOS: ~/Library/LaunchAgents/com.neosian.serve.plist -->
<plist version="1.0"><dict>
<key>Label</key><string>com.neosian.serve</string>
<key>ProgramArguments</key>
<array><string>/path/to/venv/bin/neosian</string><string>serve</string></array>
<key>EnvironmentVariables</key>
<dict><key>NEOSIAN_SERVE_TOKEN</key><string>change-me</string></dict>
<key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
</dict></plist>
# Linux: ~/.config/systemd/user/neosian.service
[Service]
ExecStart=/path/to/venv/bin/neosian serve
EnvironmentFile=%h/.config/neosian/serve.env # NEOSIAN_SERVE_TOKEN=…
Restart=on-failure
[Install]
WantedBy=default.target

Then launchctl load / systemctl --user enable --now neosian, and one run moves every client on the machine behind it:

Terminal window
NEOSIAN_CLIENT_TOKEN=change-me neosian setup --url http://127.0.0.1:6367 --write

The token is never written into a registration or a hook line: set NEOSIAN_CLIENT_TOKEN in each client’s own environment (for Claude Code, the env block of ~/.claude/settings.json; for a shell-launched client, your shell profile). Muse clears child environments: its MCP entry forwards the token by an environment reference, and setup --url configures managed hooks to receive that variable by name. This requires user-level hooks; an existing foreign managed file requires a manual merge (neosian docs agents).

Python clients speak the store wire:

from neosian import RemoteStore
store = await RemoteStore.connect("http://localhost:6367", token="change-me")

RemoteStore implements both storage ABCs over httpx alone (no serving stack loaded), so it drops into Conversation and MemoryConfig exactly where FileStore does. A client in another language implements the contract neosian docs wire states. Agents speak MCP over streamable HTTP at /mcp when the server is started with mounts (--scope or --mount).

Every listing crosses the wire a page at a time. A route answers at most 500 rows and names what to send back: next_cursor for the memory listings, next_after for a conversation’s turns and projections. RemoteStore follows the pages for you, so history(scope) still returns every row. To page yourself, the three shipped stores implement Pageable: await store.history_page(scope, cursor=None, limit=100) returns Page(items, next_cursor), where the cursor is opaque and valid only with the same arguments.

Serving your own store. build_app(store) accepts any store that implements both storage ABCs. If yours does not also implement Pageable, its listings answer whole, in one response with next_cursor: null, and a page or a cursor sent to it is refused by name. That response holds the entire listing in the process’s memory. At the KB scale memory is built for this is harmless, but a scope with millions of version rows can exhaust the process, exactly as the same call would inside your own application. Implement the four *_page methods to bound it. neosian serve only opens stores that do.

The state process asserts identity (DESIGN §20): NEOSIAN_SERVE_TOKEN may be a table (claude-code:laptop=…,app:kit=…) and every write through the wire is recorded under the presenting token’s client (<client>[/<what the client said>]); a bare token is the one client client:default. Shell entry points reach the process with --url and NEOSIAN_CLIENT_TOKEN; neosian audit --scope S reads the ledger back on any substrate, and --url reads it through the process.

The agent door reads as well as writes. A foreign agent’s SessionStart hook prints the memory index and “where we left off” (the scope’s recent sessions, log-projected) into its own window, and its MCP client calls search_history(query) and recall_turn(turn, conversation) on /mcp (or the stdio server) to find and re-read any recorded turn verbatim: one client writes, a different client recalls, on the same store (neosian docs agents, neosian docs mcp).

FileStore’s in-process lock serializes mutations inside one process; across processes, files cannot arbitrate: two writers on one root can interleave a read-modify-write and lose an edit. neosian documents the constraint instead of engineering around it: no lock files, no flock, no leases (they half-promise arbitration at the price of an NFS/Windows/containers portability matrix and a stale-lock failure mode, on the substrate whose entire value is that you can cat it).

A root is owned by one writer at a time: an agent’s shell (neosian memory), one MCP server process, or one embedding application. Any number of readers may run beside it; a concurrent reader is bounded to a stale read, never a corrupted store. Multi-writer needs route to PostgresStore, which arbitrates on the version-row primary key, or to the state process, where one neosian serve owns the files and every client speaks to it over RemoteStore.

What two projects on one home share. A machine registered once runs a hook process and an MCP server per session, all on the home, so it is worth being exact about the rule’s reach. Different scopes are disjoint files: each scope’s documents, version sidecars and redaction trail live in its own directory, and a conversation is one directory per id, so two projects, or two sessions, never write the same file by writing their own. The shared surface is the scope every project mounts, /user, and two sessions of the same project. There a race needs two writers on the same document at the same moment; what it costs is an edit lost (last writer wins) and, in the sidecar, a version number given twice, since both read the last number before either appends. Nothing tears: appends are whole lines and a document is replaced whole. If that window matters to you, the state process closes it, and moving a machine there is the one neosian setup --url run above.

  • One app, one machine, inspectable state → embed + FileStore.
  • One app, many workers or many machines → embed + PostgresStore.
  • Many apps or languages sharing one memory, or a FileStore root that needs more than one writer (the home, once two projects’ hooks or servers write it) → the state process (neosian serve).
  • Changing your mind later → neosian export from the one, neosian import into the other; the archive is a FileStore root either way.