I've been a web developer for most of my career. Laravel, mostly. CRUD apps with reasonable polish, the kind of work where the problem shapes are familiar and you can move on instinct. I'm a computer nerd in a broader sense — I appreciate the whole machine — but I'd never built desktop software. I knew about it from a distance the way you know about a country you've never visited.
This is a piece about what happened when I decided to visit.
The toy app
The origin of Persona is embarrassingly small. I built a little thing one evening that let you plug in an OpenRouter key and pit two language models against each other, masking their identities, watching them try to reach a result. I had a stupid amount of fun with it. I'd give one model a personality, the other a different one, sit back, and let them argue.
Then I started thinking: what if there were more than two characters?
What if I were one of them?
It's not a long walk from there to where Persona is now. A local-first desktop app where you build a city, populate it with characters who have personalities and relationships and memories, and just live in that world for a while. They send you photos. They remember the conversation you had three weeks ago. They argue with each other in group chats and update how they feel about each other based on what happened.
The feature I'm proudest of, almost as a thesis statement for the whole project, is something I call Global Narrate. You open a modal and type something. It can be anything. A news story. A weather event. The death of a local politician. An LLM reads your input, decides which of the characters in your city would be affected and how, and implants the appropriate memories. It also decides which character-to-character relationships have shifted. Before it commits anything, it shows you a confirmation screen. Here are the characters who'll be affected, here are the memories that will be planted, uncheck anything you don't want recorded. Then it writes.
That's the kind of feature where you can feel that the underlying systems are doing real work. The memory architecture isn't a chat history with a fancy hat on. The relationship model isn't a sentiment score in a column.
But getting there meant entering several domains I knew nothing about. So.
Choosing Rust, and what that cost me
I picked Rust deliberately. Persona is a privacy-focused app. Your conversations live encrypted on your machine, your generated images stay local, your "city backups" are sealed bundles you control. I wanted something that ran close to the metal, with strong type guarantees, where I could close off whole categories of attack vector by virtue of the language. I'd had Tauri and Wails on my radar as Electron alternatives, and I didn't need Chromium specifically, so a smaller binary that didn't bundle a full browser was attractive. Tauri won because Rust won.
The honest version of the story is that Rust has been wonderful and the compiler has been my nemesis. My CI builds Persona for three platforms in twenty minutes with aggressive caching, thirty without. I have a Go/Wails side project that builds for the same three targets in three and a half minutes flat, no cache. The iteration cycle in Rust is slow. You need to be more confident before you ship. For a security-focused app I think that's actually the right tradeoff. The language enforces a kind of patience. But the day-to-day cost is real.
If I were starting over I might pick Go/Wails. I'm not starting over. And there are real things I'd lose: the encryption surface area, the type-system discipline, the smaller binary, the easier story for shipping a desktop app I plan to keep running for years. The regret is genuine but it's the kind of regret you live with, not the kind that wakes you up at night.
The release pipeline, or: things nobody warned me about
Here's the thing nobody tells you when you decide to ship a desktop app. You will spend more time on release engineering than you will on the actual hard problems of your product.
I thought the technically gnarly stuff would be the image generation pipeline, the memory architecture, the scene-state tracking. There was plenty of that, and I'll get to it. But I lost days, and a non-zero amount of money in burned CI minutes, on getting builds to ship correctly.
The two scars I'll show you:
Windows code-signing. Took forever to integrate. The signing tool only worked if I compressed the binary using one specific zip format. It otherwise didn't care about the format at all, so pre-signing builds looked totally fine and I had no signal anything was wrong until the signing step failed in a way that made it look like the binary itself was broken. I tracked it down by elimination, not by reading documentation, because the documentation didn't mention it.
The blank Mac window. For a stretch of about a week, my macOS builds shipped without a UI. Just a blank Tauri window when you launched the app. Linux and Windows builds were fine. I burned probably ten release attempts and a tank of patience figuring out what changed in the macOS bundling step that was silently dropping the frontend assets. I don't even remember the fix anymore. What I remember is the whack-a-mole feeling. Every time I thought I had a clean release, a different thing would break on a different platform, and CI wasn't telling me anything useful until the install actually ran.
What came out of all of this is a release.yml file that is, by a wide margin, the most heavily inline-commented file in the repo. Every gotcha I learned along the way is recorded right there in the YAML, addressed not to me but to any future agent that might end up modifying the pipeline. I don't want anyone, human or otherwise, to step on the same rake twice.
That file is my scar tissue. It's also, in a weird way, one of the things I'm proudest of. Release engineering is a whole discipline I had no respect for as a web developer because I'd never had to do it. Now I do.
The actual hard technical problems
OK. The product side. Two problems that genuinely stretched me.
Per-character memory across overlapping scenes. Persona's memories are stored per character, not per chat. This is, I think, what differentiates it from most LLM roleplay systems. A character carries one memory across every conversation they're in, including group chats where they're sitting silently and not responding. The benefit is huge. Characters feel continuous. The cost is a category of bug I didn't anticipate. If I'm chatting with Character A in a baseball park, and then I open a group chat with A and B and explicitly set the scene to my office and ask where they are, Character A might still think they're in the baseball park. Location memory bleeds across chats.
Solving this took me deep into a genuinely arcane corner of academic LLM research: turn-taking and scene-state architecture for multi-agent roleplay. There's published work on this; I just had to find it. Persona 1.1 will ship with an implementation of AdaMARP, a turn-taking and shared-environment architecture from a 2026 paper that handles exactly this kind of cross-scene state coordination. I would not have found that paper if I hadn't first accepted that this was a real research domain and not a thing I should be solving from first principles.
Face consistency across multi-character images. When a character sends you a selfie, you want them to look like themselves, across that photo and every photo before and after it. Persona handles this with a per-character LoRA training step. If you've generated three reference photos you like, you can spend a few bucks training a LoRA that makes every subsequent photo of that character look identical. That works beautifully for solo shots.
It falls apart in group photos. Even when both characters in the frame have their own LoRA weights trained, you get a face that looks like a blend of the two. The kind of uncanny near-recognition where you know something is wrong but can't say what. The fix is a multi-pass pipeline: a reasoning-enabled language model acts as a "scene director" first, generating bounding boxes for where each character will appear, and then the image is generated in masked passes, one character at a time, each conditioned on its own LoRA. This is the leading industry practice right now and I'm still refining the implementation, but it works.
What both of these problems had in common, and this is the meta-point, is that they each required me to step back, do a proper research run (usually with Gemini in deep-research mode, which I find to be the most capable of the accessible research agents), bring the findings into my Claude project files, and then move from research to design to implementation as discrete phases.
That's not how I'd built software before. CRUD apps reward "just figure it out as you go." This kind of work doesn't.
The thing I'm actually proudest of
Honestly? The feel.
I did multiple full UX redesign passes on Persona, every one of them with expert UX guidance pulled from research rather than from my own taste. I am not a designer. I have functional taste but not original taste. What I had to do, and this was harder for me than the Rust learning curve, was admit that UX is its own domain with its own research and its own expertise, and that my intuition was a worse guide to it than a structured study.
The end product feels crispy. The scrolling, of all things, took multiple attempts to get right. A chat app where you enter a conversation and you're already scrolled to the bottom, except sometimes not all the way, except when there's a new message, except when... it's a deceptively complex piece of behavior and I have new respect for every chat app I've ever used.
The typing indicators feel right. The collapsible sidebar feels right. The transitions feel right. There's a lot of complex AI machinery underneath but on the surface it just feels like a beautiful chat app. That's the whole point.
What I had to unlearn
I want to be precise about this part because it's where the most useful insight is, and I think a lot of senior engineers are going to face the same thing in the next couple of years.
Roughly 85% of Persona is agent-written. I am, to this day, a person who has personally typed exactly one "hello world" in Rust. I love writing Vue and I love writing system prompts; almost everything else is dispatched.
The instinct I had to suppress, more than any other, was: I can fix this faster than I can write the prompt.
That's true for any individual change. It's wrong as a strategy. The prompt unblocks me; the fix happens in parallel. Even for one-line fixes, I'll dispatch to one of several Claude Code sessions running in parallel rather than context-switch myself into the file. The throughput gain is enormous. Senior-engineer instinct optimizes for local efficiency — I'll just do it — and that instinct is exactly wrong when you have a team of agents waiting for direction.
Calling it a team isn't a metaphor I'm using lightly. My actual mental model of how I work now:
- Gemini Deep Research is my research director. When a problem feels like it has a real domain behind it, that's where I go first.
- Claude (in chat) is my engineering manager. We strategize, design, and produce dispatch prompts together.
- Multiple parallel Claude Code sessions are my engineers, executing on dispatched prompts headlessly via shell.
- A specialized Claude project acts as a security expert that does a full pass on almost every patch.
- Occasionally, a fresh Claude conversation acts as a UX consultant conducting a study.
This setup gives me near-expertise in any domain I need it. Not real expertise — I'd never claim that — but functional access to it. Enough to ship.
The paradox
Engineers have a stereotype of big egos. I think the stereotype is half-right and half-wrong. The ego comes from a real place: from repeatedly facing a problem you understand nothing about and, with enough time and effort, eventually fully grokking it. That's a deeply earned confidence. It's also a trap.
The trap is that it makes you reluctant to say "wait, this is a whole domain. There's research for this. I shouldn't be solving it from instinct." The same confidence that lets you grok new things prevents you from outsourcing the grokking when the grokking is the wrong move.
A long career humbles you out of that. Or it should. I think the engineers who'll thrive in the next few years are the ones who can hold both at once: the confidence that says I can learn this and the humility that says I shouldn't have to from scratch.
What this work feels like now
I think the tired adage that engineering will start to look more like management is true, and I think it's a good thing.
A bit of humility goes a long way going in. Everyone you "hire" in this setup is genuinely better than you at the specific thing you're hiring them for. The reasoning model that acts as your scene director knows things about spatial reasoning under structured-output constraints that you don't, and shouldn't have to. The research agent has read more recent papers in any narrow domain than you ever will. Knowing when to ask, rather than dictate, is the skill.
I don't think "engineer" really fits as a job title anymore for this kind of work. Understanding systems architecture is still required (you can't orchestrate what you can't reason about) but the day-to-day shape is closer to agent-wrangling than to coding. The future senior IC won't boast about expertise in a language. They'll demonstrate that they know what stack to use for each task, because they understand the tradeoffs, not the syntax.
I'm not upset that the bar for novices is lower now. I'm not threatened by sixteen-year-olds shipping apps. The reason I'm not is that the ceiling, for engineers who know what they're doing, is higher than it's ever been. I built a Tauri/Rust desktop app with a multi-model image pipeline and an academic-paper-derived memory system in my spare time, having never written a desktop app or a line of production Rust before. Five years ago that takes a team. Now it takes one person who knows how to manage a team of agents.
That's the new craft. I think it's a good one.
Persona is at playpersona.app if you want to see what fell out of all of this.