Today I moved Bubbles, the family assistant, from one agent runtime to another. New runtime, new primary channel (it answers text messages now instead of living in a chat server), new model behind it.
I expected the brain transplant to be the hard part. It wasn’t. The conversation layer moved in an afternoon, because that’s the part everyone builds an abstraction for. What ate the rest of the day was everything I’d bolted onto the old one over the preceding months, none of which came along for free.
That’s the actual lesson, and I’d like to write it down while it still stings.
The migration is never the runtime
Here’s what actually had to be re-pointed, none of which is “the agent”:
- Every scheduled job, which lived inside the runtime’s own scheduler
- Every tool server — home automation, media, the network gear — each with its own launch command and config format
- The channel identity it speaks through
- The persona and behavior config
- Its skills
- A pile of helper scripts it calls
- The file-sync rules between my laptop and the machine it runs on
Seven categories, and only one of them is the model. The runtime is the thing everyone benchmarks and the thing you’ll replace soonest. Everything in that list outlives it, and every item stored inside the runtime is an item you re-do at migration time.
I now think of this as the first question to ask when adding any capability to an agent: if I replaced the runtime tomorrow, does this come with me? If the answer is no, it’s in the wrong place.
The cron jobs that expired every seven days
This one made me stop and stare.
The runtime I was leaving managed its own scheduled jobs — a morning brief, a weekly music roundup, a Friday movie pick, a media-queue cleanup. It also, by design, expired those jobs after seven days.
The mitigation already in place when I inherited my own setup: a nightly restart at 4 AM that recreated them.
Sit with that for a second. A scheduler whose jobs evaporate weekly, and a restart loop papering over it. It worked. It had been working. And it’s a genuinely bad arrangement, for reasons that have nothing to do with the seven days and everything to do with the failure mode.
Nothing errors when a scheduled job expires. There’s no exception, no alert, no red anything. The job simply stops existing, and a job that doesn’t exist doesn’t fail — it’s just absent. If the nightly restart ever missed, the first symptom would be an email that didn’t arrive at 6:33 AM. And nobody notices an email that didn’t arrive. You notice the wrong email. You do not notice the absence of a thing you’ve stopped consciously expecting, which is precisely what a daily automation becomes after about a week.
So the whole arrangement had two silent layers stacked on each other: a scheduler that quietly forgets, and a watchdog whose failure is also quiet. The only thing standing between me and losing my morning brief was a restart script nobody was monitoring.
Anything durable belongs outside the thing you’ll replace
The principle I’m taking out of today: scheduling and state should not live inside the agent runtime.
Not because this particular runtime is bad at it — because the runtime is the most volatile component in the entire stack. It’s the piece with the fastest release cadence, the piece being actively reinvented by several companies at once, and demonstrably the piece I have just replaced. Anything that must survive should live in infrastructure boring enough to still be there in a year.
A schedule is a fact about my life: I want a brief at 6:33, a movie pick on Friday. That fact has nothing to do with which agent framework is fashionable this quarter. Encoding it inside the framework means re-encoding it every time the framework changes, and it means the schedule inherits the framework’s failure modes — including the ones like a seven-day expiry that you’d never choose on purpose.
The version of this I want: a durable scheduler somewhere dumb and reliable that calls the agent, rather than an agent that remembers to wake itself up. The agent becomes a stateless thing you invoke. It can be replaced on a Tuesday without touching a single schedule.
I haven’t built that yet. Today was the migration; the schedules came along in their existing shape because moving them was a second project. But I know what the second project is now, which is more than I knew this morning.
Two smaller lessons, both about identity
Give the agent its own account. I set the assistant up with a dedicated Apple ID for messaging rather than sharing mine. This isn’t fastidiousness — using the same account causes the messaging system to merge the two identities, which makes separate conversation threads impossible. You end up with the agent’s messages and your messages in one indistinguishable stream.
The general form is worth stating: an agent that acts in your accounts should have its own identity wherever the platform allows it. You get threading, you get attribution when you’re reading back through what happened, and you get a revocation story that isn’t “change my own password.” Sharing your identity with something that acts autonomously is the kind of shortcut that’s free for exactly as long as nothing goes wrong.
Sync the portable, exclude the machine-specific. My laptop and the Mac Mini sync some config between them, and their home directories have different usernames. Anything containing an absolute path is therefore correct on one machine and silently wrong on the other — no error, just a config pointing at a directory that doesn’t exist. That had produced more than eighty sync-conflict files before I looked.
The fix was an ignore file on both ends excluding machine-specific paths, which is easy. Knowing to look was the hard part, and the tell was subtle: not a crash, just plugin configs that behaved slightly differently depending on which machine you were sitting at.
What I’d do differently
If I were standing this up from scratch instead of migrating it, the line I’d try first is: the runtime gets the conversation loop and the tool calls, and little else. Schedules, persistent state, and identity would live outside it. That would make the runtime the component I’m willing to throw away, because replacing it should cost an afternoon instead of a day.
I’m writing this on migration day, so I’ll admit the obvious: this is unlikely to be the last runtime this assistant runs on. The field is moving too fast for anyone to call it settled. For now, the more useful design question is not which runtime — it is what I can keep outside whichever one I try next.