Restoring TARS: Moving a Self-Hosted AI Agent to a New Box
I run a self-hosted AI agent named TARS. It is a local second brain: it reads my notes, runs scheduled jobs, manages my task list, and answers questions without shipping any of that to somebody else’s cloud unless I specifically decide it should. The stack is the open-source Hermes Agent with a web UI on top, a local model served by Ollama, and a cloud model wired in as a fallback for when the local box is too slow or too stupid for whatever I just asked.
I could have paid a company to do all of this and never thought about it again. I did not, because then a company would know everything my second brain knows, and I would rather it did not. So instead I did the work myself, and then I did it a second time on another machine, and the second time is the part worth writing down.
The shape of the thing
The core is the same on both boxes:
- Hermes Agent plus a web UI, each sealed in its own Python 3.13 virtual environment so they cannot ruin each other.
- A local model via Ollama for background and scheduled work. On the Mac that is qwen3.6, on the Linux box a mix of qwen3.6 and gemma.
- A cloud model as the fallback, Claude Haiku, which takes over when the local model rate-limits, errors, or would take so long I have died of old age waiting.
- Auto-start on boot, so the agent and the web UI come back after a reboot without me standing over them. On macOS that is launchd. On Linux it is systemd user units with lingering turned on.
- An identity file, a plain SOUL.md that loads before the main config and makes the agent TARS instead of a generic assistant with no opinions.
On the Linux box I put a Caddy reverse proxy in front of the web UI so it gets real TLS, and joined it to a Tailscale network so I can reach it from anywhere without opening a port to the entire internet. The web UI binds to localhost and Caddy does the talking. This is the correct amount of paranoia.
The part that makes it a brain: Obsidian
None of the above is a second brain on its own. It is a very elaborate way to talk to a model. The brain is the data, and the data lives in an Obsidian vault, a folder of plain Markdown files that is the actual source of truth for my notes, my plans, and my task list.
TARS reads that vault. It does not just answer trivia, it answers questions about my life, because the vault is where my life is written down. A small script gives it read and write access to the task list inside the vault, so it can add, check off, and reorganize tasks as plain Markdown instead of trapping them in some app’s database I cannot back up. Everything is a file. Files I understand. Files I can move to another machine. Which, as it turns out, is the whole point of this post.
One warning, which becomes a theme: when I built the second instance, I copied the vault over exactly once and set up no ongoing sync. So now there are two brains slowly drifting apart, each remembering a slightly different version of my life. If you do this, decide on purpose whether the two vaults sync or stay separate. Do not decide it by accident, the way I did.
Moving it to a new box
The Mac instance is backed up to a private GitHub repository: config, personas, the SOUL.md identity, the restore script. I am not linking it here, for reasons I hope are obvious, but the point is that GitHub is quietly holding a full copy of the setup somewhere that is not the machine itself. Restoring on the same machine is nearly one command. Pull the backup, run the restore script, and it rebuilds both virtual environments, pulls the local model, drops the config and identity into place, installs the auto-start services, and health-checks itself. Very satisfying. Almost suspicious how smoothly it goes.
The second machine is a System76 Meerkat, and that restore was not smooth, because it was a different operating system, a different init system, and a newer version of the agent. Here is what actually cost me time.
The provider got renamed and silence is the failure mode
Between the version on my Mac and the version on the new box, the local-model provider was renamed. The old name still parsed in the config. It just did nothing. The agent did not error out, because that would have been helpful. It quietly fell back to a stock configuration, booted as a generic assistant instead of TARS, and left the tools half-broken. The lesson: when a self-hosted agent loses its personality for no obvious reason, suspect the provider config before anything else. The loud failures are easy. The polite, silent ones are the ones that ruin your evening.
The 4096 context trap
The OpenAI-compatible provider path cannot pass a context-length parameter down to Ollama. So Ollama does what it does by default and loads the model with a 4096-token context window. My system prompt alone is bigger than that. The front of the prompt, which is exactly where the identity and the tool definitions live, gets silently chopped off. Same symptom as the provider bug, different cause: the agent is technically running, it is just no longer itself. The fix is to set the context length on the Ollama service directly, or bake the parameter into a derived model, instead of trusting the provider to forward it. It will not.
Reasoning models eat their own budget
The local models I run are reasoning models. Hand one a tiny output-token budget and it spends the entire thing thinking, then returns an empty response with a “length” finish reason and no actual answer. It looks exactly like a broken model. It is not broken. It just used up all its words having feelings about the problem before it got around to solving it. I relate to this more than I would like. Give it a real budget.
Hardware is honest
The Meerkat is a nice little machine, but it is a laptop-class CPU with no discrete GPU and no way to add one. Local model turns take minutes, not seconds, because a large system prompt on a CPU evaluates slowly. This is fine, because on that box the local model only ever does background and scheduled work. Anything interactive falls through to the cloud model. Know what a box is for before you get annoyed that it is not something else.
Back up the second box too
The mistake I most want to save you from: the new machine never got the same GitHub backup the original did. Its config survives only as local timestamped backup files, which is fine right up until the disk is not fine. If a setup is worth restoring once, it is worth being restorable again, which means the second instance needs the same off-machine backup as the first. I know this. I skipped it anyway. Do as I say, not as I did.
What I would tell past me
The build is the easy part. Every interesting failure came from the gap between two machines: a renamed provider, a default that truncated my prompt, a budget that starved the model, a vault I copied once and forgot, a backup I never made. None of them threw a clean error. Every one of them showed up as “the agent is up but acting wrong,” and every one wasted time because the symptom pointed nowhere near the cause.
So the real takeaway is not the command list. It is this: for a self-hosted agent, “running” and “working” are two different states, and the space between them is where your afternoon goes to die. Confirm the identity loaded. Confirm the context window is real. Confirm the model has room to answer. Confirm the data actually came with it. Then, and only then, believe it is fine.
TARS is back up on both boxes now. It is, as always, completely unbothered by any of this, which is the one part of the design I got exactly right.
Comments
Be the first to comment.