A Personal Agent That Picks Up Where It Crashed
Tags:#ai#agentsThe first two things I wanted from a personal agent were reminders and a way to ask “is the homelab OK?” from my phone. Reminders are the tricky one. A reminder for next Tuesday, a question the agent is waiting on me to answer, a job that runs every weekday morning: all of them have to survive the server going away, and servers go away. They get redeployed and updated, and sometimes the process just dies.
gaidin is built on Earendil’s Pi Durable, which is built for that. I started it on October 8 as one agent running alongside Hermes, the agent I already had. Nothing moves over from Hermes until gaidin earns it. Two weeks later it runs five agents, has its own app, and the code is on GitHub under the MIT license.
What durable means here
Pi Durable stores a conversation as entries (my messages, the model’s answers, tool results) and runs all the work as tasks that save a checkpoint after every step. Everything lives in one SQLite file, owned by one server process. Kill that process at any point and it resumes from the last checkpoint when it starts again.
In practice:
- A run that was generating, or waiting on a tool, picks up again.
- A reminder is a background task that sleeps until its time. After a restart it goes back to sleep until the same moment. If it came due while the server was down, it fires as soon as the server is back.
- A Slack message is written to storage before Slack is told it arrived, so one that came in during a crash still gets an answer.
- An approval keeps its id across a restart, so the same tool call waits on the same request instead of asking me twice. A run can wait days for me this way and costs nothing while it waits.
Killing it on purpose
For these screenshots I ran gaidin’s test server, which swaps the real model for a scripted one, so the replies are canned. The agents have my real agents’ names and jobs. I set a reminder, scheduled a weekday briefing and asked Lan, my main agent, to fetch a page on a host it hadn’t seen before. Fetching from a new host needs my approval, so the run stopped and waited:

Then I killed the server with kill -9, with no chance to clean up or flush anything. The app noticed and started reconnecting. The approval stayed on screen because the app had it, but nothing on the server side was alive to act on it:

I started the server again. The app reconnected, the same request was still waiting, and clicking Allow once finished the original run:

Not every tool can run twice
Resuming after a crash means something might run twice. Pi Durable makes you mark each tool as safe to replay or not. Fetching a page is safe, so fetch_url says so:
const fetchUrl = defineTool({
name: 'fetch_url',
// ...
replay: 'safe',
execute: async (args) => { /* ... */ },
})
bash isn’t marked safe, since running rm or git push twice is a different thing than running it once. If the server dies in the middle of a shell command, gaidin doesn’t run it again. The model is told the command was interrupted and may have partly run, and it can work out what to do from there.
Delivery is at least once. If gaidin crashes after sending a push notification but before writing down that it sent it, I get that notification twice. I’ll take a duplicate reminder over a missing one.
One agent became five
gaidin now keeps a roster of named agents. Each one has its own home conversation, sandbox, work notes, reminders, jobs and grants, and no agent can list, cancel or search another’s. They share the spend limits and a set of notes about me, except Siuan, which reads strangers’ email and doesn’t get the notes.
- Lan, the original, does reminders, briefings, the homelab and anything that isn’t another agent’s job.
- Verin researches questions on the web and reports back with sources and what’s still uncertain.
- Min thinks through ideas with me. Conversation, not chores.
- Ward checks my projects for vulnerable dependencies and leaked secrets, and prepares fixes on branches for me to review and push.
- Siuan keeps my Proton Mail inbox clear: it archives bulk mail, flags scams and tells me only what needs me.

Lan can message or hand work to Verin and Ward, Min can reach Lan, and no other agent can reach another. A message between agents counts as outside text in both directions, and messaging asks me while there’s outside text in the sender’s conversation, so a page Lan read can’t quietly pass work to Ward, which can write to my projects. A chain stops after three hops.
The web view I started with is gone. Its replacement is moiraine, a TanStack Start app with the agents in a rail down the side, each one’s threads beside it, and pages for schedules, notes, rules and spend. It installs on my phone and gets push notifications. Slack still works, with a channel per agent, and every top-level message there starts a thread that’s a conversation of its own.
Anything that reaches out asks first
The agents read text I didn’t write: web pages, logs, email, files in my projects. Any of it can contain instructions, so gaidin is built assuming the model can be steered. Fetching from a new host, scheduling a job, working on one of my other machines and loading a tool an agent wrote itself all wait for me. So does writing a note to memory when there’s outside text in the conversation.
Shell commands run under bubblewrap with no network, and an agent’s own sandbox directory is the only place they can write. The server’s secrets are scrubbed from shell output as it streams and from every tool result. fetch_url refuses private, loopback and tailnet addresses, checked when it connects and again after each redirect. gaidin’s web server listens only on localhost, with Tailscale Serve in front, and answers only my login.
Three additions since the first version change what reaches out and what asks:
- A browser. Lan has a Chromium profile for sites that need a real page. Opening a page asks the way a fetch does, and clicking or typing after reading a page asks too, unless I’ve let Lan act on that site. The model never types a password. When a site needs a sign-in, Lan asks me to take the profile over, and I do it from a live view in the app, on my phone if that’s where I am.
- Connectors. These are remote MCP servers: Home Assistant for Lan, and a mail connector for Siuan. I set each tool to allow, ask or off, and each tool’s definition is pinned by a hash, so one that changes on the server’s side is off until I set it again.
- My own rules. I can write allow and ask rules per agent and tool, with conditions on the call’s arguments. Min can read, write and edit anything in my Ideas folder without asking. Verin can write to my Learnings notes. Lan can turn devices off through Home Assistant until it reads outside text. One rule goes the other way: Siuan reads untrusted mail all day and a reminder lands on my phone, so Siuan’s reminders always ask.
No rule can let an agent load code it wrote, change a skill, schedule a job or upload my files from the browser. Those always ask.
Every approval and what came of it is written into the conversation, and the activity page turns that into a timeline across all the agents:

The repo has a security doc with a list of known gaps, and it’s longer than it was. A host I’ve allowed for good can still receive data in a URL without asking. Secret masking in workspaces goes by file name. Anything gaidin writes into a project can run later as me. Agents are kept apart by the server’s rules, not separate processes or users, so a bug in those rules would let one reach another’s files.
Smaller things
- Spend limits. It defaults to Haiku 5.5 with a daily and monthly dollar limit. When it hits one, every run stops and says why, and the app has a button to allow another day.
- Compaction. Pi Durable times summarizing old messages from the model’s context window, and for a 1M-token model that means a conversation never compacts. Mine got past 100,000 tokens, which is the point where requests get billed at a higher rate. gaidin now starts a summary in the background at about 60,000 tokens and won’t send a request over 85,000.
- Time in the message, not the prompt. The current time goes in brackets at the start of each message instead of in the system prompt, so the provider’s prompt cache stays valid between messages.
- Voice. There’s a token-gated route for a speaker in the house to talk to an agent in text, with speech-to-text and text-to-speech handled outside gaidin. Anything the speaker heard counts as outside text, and the household agent behind it doesn’t see the notes about me. That agent is archived for now.
It’s TypeScript on Node with no web framework on the server, SQLite through Pi Durable, and one long-lived process on its own VM. The VM’s nightly backup covers the SQLite file, and that file is all five agents.
In the Wheel of Time, a Warder (gaidin) is bonded to one Aes Sedai and always on watch. Mine just have to stay up.