<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Luca</title>
    <description>The latest articles on DEV Community by Luca (@ilmartotch).</description>
    <link>https://dev.to/ilmartotch</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3759283%2Fcfb115d0-89b3-4576-ab47-88b6b98c4c82.jpg</url>
      <title>DEV Community: Luca</title>
      <link>https://dev.to/ilmartotch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ilmartotch"/>
    <language>en</language>
    <item>
      <title>Architecture-first vs problem-first: what five months of over-engineering looks like</title>
      <dc:creator>Luca</dc:creator>
      <pubDate>Mon, 13 Jul 2026 18:24:22 +0000</pubDate>
      <link>https://dev.to/ilmartotch/architecture-first-vs-problem-first-what-five-months-of-over-engineering-looks-like-4jbd</link>
      <guid>https://dev.to/ilmartotch/architecture-first-vs-problem-first-what-five-months-of-over-engineering-looks-like-4jbd</guid>
      <description>&lt;p&gt;Why build something? And what if nobody ends up using it?&lt;/p&gt;

&lt;p&gt;There are good answers to the first one. You build because you need a thing that doesn't exist yet. You build to see if you can, the technical challenge, the "is this even possible?" You build to impress someone, or just because you think it'll make people's day a little less annoying.&lt;/p&gt;

&lt;p&gt;All of those are real reasons, and at different points, I told myself most of them.&lt;/p&gt;

&lt;p&gt;Then, a few days ago, late in the day, at the end of a coding session, five months into the project, I asked myself those two questions back-to-back. And for the first time, I couldn't answer the second one.&lt;/p&gt;

&lt;p&gt;Zeri worked. Every feature did what it was supposed to do. Both processes handshake cleanly, a variable set in one context showing up in another a second later, the TUI rendering exactly as I'd pictured it. And I sat there and couldn't come up with one honest sentence explaining why anyone would actually download it.&lt;/p&gt;

&lt;p&gt;That gap, between something built well and something that has a reason to exist, turned out to be the most useful thing this whole project taught me. So I'm shipping it anyway, and I'll tell you why.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;Zeri is a TUI multi-language REPL. You launch it, pick a language, &lt;code&gt;Python&lt;/code&gt;, &lt;code&gt;JavaScript&lt;/code&gt; (with &lt;code&gt;Bun&lt;/code&gt;), &lt;code&gt;Ruby&lt;/code&gt;, or &lt;code&gt;LuaJIT&lt;/code&gt;, and you get an interactive session in your terminal. You can switch languages mid-session, share variables across them, save and reload your work, manage snippets, and talk to a local LLM through a command running on &lt;code&gt;Ollama&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The feature list isn't the interesting part, though. The interesting part is what's underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two processes, one app
&lt;/h2&gt;

&lt;p&gt;Zeri is split into two processes: a headless engine written in &lt;code&gt;C++23&lt;/code&gt; and a TUI frontend built in &lt;code&gt;Go&lt;/code&gt; using &lt;code&gt;Bubble Tea&lt;/code&gt; and &lt;code&gt;Lip Gloss&lt;/code&gt;. The engine does all the evaluation, state, and runtime coordination. The frontend does rendering, input, and everything the user actually sees and touches. They talk to each other over a custom binary IPC protocol that I built from scratch.&lt;/p&gt;

&lt;p&gt;Why split them at all? Separation of concerns, but taken seriously instead of as a buzzword. The engine has no idea what's rendering its output; it could be my TUI, could be a web UI, could be an editor plugin, doesn't care. And the frontend has no idea how code actually gets evaluated. The nice side effect is that the engine can't crash the UI and the UI can't freeze the engine, because each one runs its own lifecycle in its own process.&lt;/p&gt;

&lt;p&gt;I went with &lt;code&gt;C++&lt;/code&gt; for the engine because I needed tight control over memory and process management, the engine spawns and babysits multiple language runtimes, and I wanted to be close to the metal for that. &lt;code&gt;Go&lt;/code&gt; for the frontend because the Charm stack is honestly the best TUI tooling I've found in any language, with a strong focus on the UX/UI part that is necessary. &lt;code&gt;Go&lt;/code&gt;'s concurrency model makes handling async IPC messages way less painful than it would be elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The protocol
&lt;/h2&gt;

&lt;p&gt;The thing connecting the engine and the frontend is a custom binary protocol. Every message is a fixed header, payload length, message type, sequence number, and then the payload. The framing is binary on purpose: the receiver reads four bytes, knows exactly how long the message is and what kind it is, and grabs the rest without parsing anything to find where the message ends.&lt;/p&gt;

&lt;p&gt;The payload inside that frame is just JSON, and that split is deliberate. The framing runs constantly and has to be cheap, so it's binary. The payload I want to read in a log and debug is JSON. Fast where it has to be, simple where it can afford to be. Chasing binary all the way down would've bought me almost nothing and cost me every debugging session for five months.&lt;/p&gt;

&lt;p&gt;That protocol came out of Zeri, and I pulled it into a standalone library, Yuumi, with a dedicated SDK per language. This is turning into a real rabbit hole; we'll see how it goes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sidecar runtimes
&lt;/h2&gt;

&lt;p&gt;Each language runs as its own sidecar process. Switch to &lt;code&gt;Python&lt;/code&gt;, Zeri spawns or reuses a &lt;code&gt;Python&lt;/code&gt; process; run some code, the engine routes it to the right sidecar, grabs the output, and ships it back to the frontend over IPC.&lt;/p&gt;

&lt;p&gt;Think of it like a restaurant kitchen. The engine is the head chef calling out orders; each sidecar is a station, one does &lt;code&gt;Python&lt;/code&gt;, one &lt;code&gt;Ruby&lt;/code&gt;, one &lt;code&gt;Lua&lt;/code&gt;. If the fry station catches fire, the pasta station keeps going (pasta, because I'm Italian). A segfault in one runtime doesn't take down the others: they're fully isolated, each with its own memory and lifecycle. The engine stands in the middle, orchestrating, handling timeouts and failures when a station goes down.&lt;/p&gt;

&lt;h2&gt;
  
  
  SharedScope
&lt;/h2&gt;

&lt;p&gt;The piece I'm proudest of, technically, is SharedScope, sharing variables across languages. You set a value in &lt;code&gt;Python&lt;/code&gt;, switch over to &lt;code&gt;JavaScript&lt;/code&gt;, ask for it, and it's there. Sounds simple. It is absolutely not simple, and explaining why is the most interesting part of the whole engine.&lt;/p&gt;

&lt;p&gt;The problem is that a value in one language isn't the same object in another. A &lt;code&gt;Python&lt;/code&gt; int and a &lt;code&gt;JavaScript&lt;/code&gt; number are different things in memory. &lt;code&gt;None&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;, and &lt;code&gt;nil&lt;/code&gt; are three different ways of saying nothing. A dict isn't an object isn't a table. So there's no "just pass the variable"; something in the middle has to own a neutral version of that value that doesn't belong to any single language.&lt;/p&gt;

&lt;p&gt;That something is the &lt;code&gt;C++&lt;/code&gt; engine. It works like a hub: every language runtime is a spoke, isolated, with its own namespace, and the engine sits in the center holding the canonical copy of every shared value as a language-agnostic type. When &lt;code&gt;Python&lt;/code&gt; calls &lt;code&gt;set&lt;/code&gt;, the value gets serialized to JSON, shipped to the engine over the protocol, and rebuilt into that neutral representation, a normalized form where every int becomes a 64-bit integer, every float a double, strings, booleans, null, and arrays and maps built recursively out of those. When &lt;code&gt;JavaScript&lt;/code&gt; later calls &lt;code&gt;get&lt;/code&gt;, the engine serializes its canonical copy back out, and the JS side rebuilds it natively. The runtimes never touch each other. They only ever talk to the hub.&lt;/p&gt;

&lt;p&gt;Two things I decided on purpose matter more than they look. First, it's copy by value, not shared memory. You're not getting a live reference; modify the object on the &lt;code&gt;JavaScript&lt;/code&gt; side, and the &lt;code&gt;Python&lt;/code&gt; side has no idea. If you want the change to propagate, you set it again. Pretending otherwise would've meant faking shared memory across four separate processes, which is a lie waiting to crash. Second, if a value doesn't fit the supported set, a function, a class instance, a &lt;code&gt;Python&lt;/code&gt; set, a &lt;code&gt;BigInt&lt;/code&gt;, or an integer too large for 64 bits, the engine doesn't quietly mangle it into something wrong. It refuses, explicitly, and the &lt;code&gt;set&lt;/code&gt; call fails on the spot. I'd rather it break loudly than hand you corrupted data three steps later.&lt;/p&gt;

&lt;p&gt;So the scope is honest and narrow: primitives and simple containers, by value, with hard edges where the languages stop agreeing. Within that box, it's reliable, and getting it reliable, the type normalization, the failure modes, the request/response handshake underneath it, is the hardest engineering in the project.&lt;/p&gt;

&lt;p&gt;And it works. A variable I wrote in &lt;code&gt;Python&lt;/code&gt;, read back in &lt;code&gt;JavaScript&lt;/code&gt;, a second later, exactly right.&lt;/p&gt;

&lt;p&gt;It was both interesting and frustrating to create this software, and it was fitting to describe it so comprehensively. And that's precisely what makes the next section so annoying to write.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;Everything worked. The handshake was clean, SharedScope moved values across languages without choking, sessions saved and restored, the AI thing answered, the TUI looked great. I could demo every single feature, and every single one did its job.&lt;/p&gt;

&lt;p&gt;But I kept landing back on the same question, and it would not leave me alone: who is this for?&lt;/p&gt;

&lt;p&gt;I tried to answer it honestly. A developer who works across multiple languages. Okay, but when exactly? If I'm writing &lt;code&gt;Python&lt;/code&gt;, I open a &lt;code&gt;Python&lt;/code&gt; REPL. If I'm in &lt;code&gt;JavaScript&lt;/code&gt;, I open &lt;code&gt;Node&lt;/code&gt; or &lt;code&gt;Bun&lt;/code&gt;. The moment I need both, I open two terminals, and that costs me about three seconds. Zeri's cross-language sharing is cool, but when was the last time I actually needed to set a variable in &lt;code&gt;Python&lt;/code&gt; and read it back in &lt;code&gt;JavaScript&lt;/code&gt; inside a REPL? I couldn't remember it ever happening.&lt;/p&gt;

&lt;p&gt;Session persistence is useful, except that most REPLs already have history. The AI integration is handy, except &lt;code&gt;Ollama&lt;/code&gt; already ships a CLI. Snippet management is nice, except so is a file.&lt;/p&gt;

&lt;p&gt;Every feature was built well. None of them answered the question. I'd built a pile of solutions and gone looking for a problem afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I missed it for five months
&lt;/h2&gt;

&lt;p&gt;The honest answer is that I never asked. Zeri started as an architecture flex. "Can I build a two-process TUI app with a custom IPC protocol?"&lt;/p&gt;

&lt;p&gt;The answer turned out to be yes, and the momentum of building just carried me for five months straight. Every week handed me a new interesting problem: serialization, process management, runtime orchestration, cross-language type mapping. I was learning &lt;code&gt;C++23&lt;/code&gt;, learning &lt;code&gt;Go&lt;/code&gt;'s concurrency patterns, designing a binary protocol from nothing. The work was real, and I got better at my job because of it.&lt;/p&gt;

&lt;p&gt;But not once did I stop and ask the boring question: what specific frustration does this kill? What's painful right now that Zeri makes painless?&lt;/p&gt;

&lt;p&gt;I know what that question sounds like when it has an answer, because my other projects all have one. One of them exists because sharing dumb code before it's PR-ready is awkward; you're pasting into Slack or screen-sharing, both terrible for async. Another exists because I lose the first two hours of every new project to the same scaffolding I've done a hundred times. Another because I watch myself burn tokens sending bloated prompts to an LLM with no tooling to tighten them up first.&lt;/p&gt;

&lt;p&gt;Each of those starts with "I keep running into this." Zeri starts with "I wonder if I can build this." That's the whole gap. Not skill, not effort, origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture-first vs problem-first
&lt;/h2&gt;

&lt;p&gt;I've started calling these two modes architecture-first and problem-first, and you can probably already guess the split: one puts solving a real problem front and center, the other aims at "the best possible architecture nobody asked for", basically a very complicated paperweight.&lt;/p&gt;

&lt;p&gt;Problem-first: something annoys you, you build the smallest thing that makes it stop. The problem leads, easy.&lt;/p&gt;

&lt;p&gt;Architecture-first: you build the cool thing, then go looking for a reason it should exist. The justification limps along behind; all of this breaks at some point.&lt;/p&gt;

&lt;p&gt;Zeri is exactly this. Every other project I've ever had or started starts with a problem, even a small one. Five months of architecture, implementation, and searching for a final goal. Good execution. Beautiful. Now what?&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually means
&lt;/h2&gt;

&lt;p&gt;I don't think this is just my problem. I think it's a recurring pattern, and I think many developers fall for it, especially self-taught developers building a portfolio, which is exactly my situation. The goal of impressing with a perfect project overrides the purpose of our development.&lt;/p&gt;

&lt;h2&gt;
  
  
  What five months of working alone taught me
&lt;/h2&gt;

&lt;p&gt;When you're learning systems programming, there's a real high in making low-level things work. Two processes talking over a protocol you designed yourself is satisfying. Serializing values across language boundaries is a proper puzzle. The feedback loop never stops: something breaks, you fix it, you learn something, repeat. Five months of that is a genuinely good education.&lt;/p&gt;

&lt;p&gt;The trap is that building feels like progress even when you're going nowhere in particular. Every bug fix, every feature, every refactor is real work; it's just not necessarily pointed at anything. And from the inside, you can't tell the difference.&lt;/p&gt;

&lt;p&gt;Working solo makes it worse. Nobody reviews your code, nobody asks "wait, why are we building this this way?" You get fast at debugging, and you start trusting your own judgment, but you lose the one thing that would've saved you: someone to ask the uncomfortable question. It's like spending a whole Sunday slow-cooking a ragù from scratch, perfect technique, six hours on the stove, and only realizing at dinner that nobody at the table eats meat. Good execution, bad dish. Five months alone is exactly how you end up cooking that ragù.&lt;/p&gt;

&lt;p&gt;So if you're building solo, find someone who'll ask why your project gets to exist at all, not about your code, but about the thing itself. Write it up and post it on Dev.to, Hacker News, Reddit. You might get the answer you couldn't reach on your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to stop and ask
&lt;/h2&gt;

&lt;p&gt;If I did this again, I'd put a checkpoint at the one-month mark, not to review the code, but to pressure-test the thesis. Can I say in one sentence who this is for and what it fixes? If I can't, that's not a signal to quit. It's a signal to stop typing and think. Maybe the answer's there, and I just haven't found the words. Maybe it isn't, and I should call it a learning project out loud instead of pretending it's a product. Either one is fine. Spending five months not asking is the part that isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do you do with something "empty"?
&lt;/h2&gt;

&lt;p&gt;I could've just shelved Zeri. I didn't, for three reasons.&lt;/p&gt;

&lt;p&gt;The engineering is real and worth showing. If you're building a portfolio and job hunting, something that proves you can think at the systems level has value even when it isn't a product. The lesson is worth sharing because the architecture-vs-product question barely comes up in "what I learned building X" posts, which are usually all about technical decisions and rarely about whether the thing should exist. And open source is unpredictable; maybe somebody looks at this and sees a use case I'm too close to see. Shipping it keeps that door open. Not shipping it closes it for no reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes
&lt;/h2&gt;

&lt;p&gt;The Zeri repo is open on my GitHub: &lt;a href="https://github.com/ilmartotch/ZeriReplEngine" rel="noopener noreferrer"&gt;github.com/ilmartotch/ZeriReplEngine&lt;/a&gt;. I'm deliberately not calling this a launch, because it isn't one. It's a portfolio piece and an open invitation: here's what I built, here's what building it taught me, here's what I'd do differently next time.&lt;/p&gt;

&lt;p&gt;Yuumi, the protocol, is turning into its own library, and funnily enough, it has a clearer identity than Zeri ever did. It's one of the better things to come out of all this. The rest of my projects are in the pipeline.&lt;/p&gt;

&lt;p&gt;Five months. A solid architecture. One genuinely useful lesson. I'd do it again. I would simply have asked the question much earlier and differently.&lt;/p&gt;

&lt;p&gt;I have two years of experience in this (software engineering), so take all of it with you and keep that in mind. I'm not handing down lessons from a mountain, I'm telling you what I figured out the slow and expensive way and betting it's useful to somebody a few steps behind me.&lt;/p&gt;

&lt;p&gt;I'm ready for the salt or the flame; in the end, I'm a League of Legends player.&lt;/p&gt;

</description>
      <category>career</category>
      <category>programming</category>
      <category>showdev</category>
      <category>learning</category>
    </item>
    <item>
      <title>Designed to survive: how Zeri's core assumes everything can die</title>
      <dc:creator>Luca</dc:creator>
      <pubDate>Mon, 13 Jul 2026 18:24:07 +0000</pubDate>
      <link>https://dev.to/ilmartotch/designed-to-survive-how-zeris-core-assumes-everything-can-die-5hhn</link>
      <guid>https://dev.to/ilmartotch/designed-to-survive-how-zeris-core-assumes-everything-can-die-5hhn</guid>
      <description>&lt;h2&gt;
  
  
  The value that belongs to no one
&lt;/h2&gt;

&lt;p&gt;The project structure is like a kitchen in mid-service. Four stations, four cooks, and none of them speak the same language. The one on the grill thinks in Python. Pasta speaks JavaScript. Garde-manger works in Ruby, and the dessert chef only knows Lua. An order comes in that needs all four to touch the same plate. Now: how do you pass a half-finished dish between people who can't read each other's tickets?&lt;/p&gt;

&lt;p&gt;That's the problem Zeri spends most of its complexity solving. It's a terminal REPL where you can start a value in one language and finish it in another, set a dictionary in Python, read it back in JavaScript, and change it in Lua. Sounds small. It isn't, because a value in one language simply is not the same object in another. A Python &lt;code&gt;int&lt;/code&gt; and a JavaScript &lt;code&gt;number&lt;/code&gt; are different things in memory. &lt;code&gt;None&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;, and &lt;code&gt;nil&lt;/code&gt; are three different ways of saying nothing, and they don't agree on much beyond the sentiment. Somebody in the middle has to own a neutral version of that value, one that belongs to no language, so it can be handed to any of them.&lt;/p&gt;

&lt;p&gt;The companion piece to this article argues about why I built a thing like this before I had a user who asked for it. This one is about the how, and the how turned out to have a single idea running under every part of it, so I'll say it once up front and then let it resurface: every boundary in this system is built assuming the other side can die. Two processes, two protocols, four sidecars, one shared hub, and each seam is designed around the possibility that whatever's on the far end goes away mid-sentence. Not as a failure case bolted on afterward. As the thing that decides the shape.&lt;/p&gt;

&lt;p&gt;None of what follows is theory. Every claim is a line I can point at, and a couple of them are lines I had to fix while writing this, which, honestly, is the best argument I know of for writing the article at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two processes that agree to die together
&lt;/h2&gt;

&lt;p&gt;Zeri runs as two processes. There's a headless engine written in C++ that does evaluation, holds state, and supervises the language runtimes. And there's a terminal UI written in Go, built on the Charm stack, that does rendering and input. They talk over a socket. That's the whole topology.&lt;/p&gt;

&lt;p&gt;The split isn't decoration. The engine has no idea what renders its output; the UI has no idea how code gets evaluated. That ignorance is the point; it means a crash on one side can't corrupt the memory of the other, because there's no shared memory to corrupt. Two address spaces, one pipe between them.&lt;/p&gt;

&lt;p&gt;Why C++ for the engine specifically? Because the engine is the thing that owns other processes. It forks runtimes, wires up their file descriptors, sets death signals, and builds job objects. That's memory-and-process work, and it's the job C++ is actually good at. Go got the frontend for the opposite reason: an event-driven TUI wants cheap concurrency and good tooling, and the Charm stack is built for exactly that shape.&lt;/p&gt;

&lt;p&gt;Now the interesting part, and the first place the "assume death" rule shows up, with a twist I didn't expect when I started. The two sides do not survive each other symmetrically. The frontend survives a dead engine: if the engine process crashes, the UI catches it and transitions to a disconnected state through two independent paths (one watching the process, one watching the transport). But the engine does not survive a frontend that walks away. Any I/O error on the bridge triggers a shutdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;bridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_error&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;bridgeTerminal&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;yuumi&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;bridgeTerminal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;RequestShutdown&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That call unblocks the REPL read loop with an empty value; the loop breaks, and the process exits. This is deliberate. An engine with no frontend attached is a headless process nobody can see, and nobody can kill cleanly. So the engine chooses to die with the thing it serves rather than outlive it. The rule isn't "nothing ever dies." The rule is "process isolation stops a crash from spreading corruption, and past that, dying on purpose beats staying." Symmetry would have been easier to write and worse to run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwopek1m1y1mh9blieaa6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwopek1m1y1mh9blieaa6.png" alt="Diagram: the Go TUI and the C++ engine as two separate processes joined by one socket. The UI watches the engine through two paths that both lead to a disconnected state; the engine, on any bridge error, calls RequestShutdown and exits on purpose. Death is observed on one side, chosen on the other." width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two protocols, because there are two kinds of "other side"
&lt;/h2&gt;

&lt;p&gt;Here's a design decision that looks like duplication until you see why it isn't: Zeri has two internal protocols, one for the UI-to-engine link, one for the engine-to-runtime link. I could have reused one for both. I didn't, and the reason is that the two boundaries are talking to two different kinds of unreliable.&lt;/p&gt;

&lt;p&gt;The first protocol carries the UI-to-engine conversation. Its frame is a fixed six-byte header, four bytes of payload length in little-endian, one channel byte, one flags byte, followed by a JSON payload.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frameHeaderSize&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LittleEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PutUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;frameHeaderSize&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header is binary because binary is cheap and unambiguous: read the first four bytes, you know exactly how many bytes the rest of the message is, and you take that many without ever inspecting a single character of the payload to find where it ends. The payload is JSON because JSON is easier to debug: when something goes wrong, you can read the message. Binary where it has to be fast, text where it can afford to be simple. That split runs through both protocols.&lt;/p&gt;

&lt;p&gt;The frame carries a length, a channel, and a flags byte, and nothing else: no sequence counter, no framing metadata beyond what it takes to find the next boundary. The channel byte does the multiplexing, so control traffic and data traffic share one socket without stepping on each other. The flags byte is reserved: there's an enum declaring &lt;code&gt;Compressed&lt;/code&gt; and &lt;code&gt;Encrypted&lt;/code&gt; bits, but nothing sets them yet. It's a byte I designed room for and haven't spent, which is cheaper than a protocol change I'd have to make later to claw the room back.&lt;/p&gt;

&lt;p&gt;The read side is where the "assume death" rule gets concrete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;asio&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_transport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;asio&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ec&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_MESSAGE_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;asio&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_transport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;asio&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ec&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every failure is a &lt;code&gt;break&lt;/code&gt;. A length that exceeds the cap is a &lt;code&gt;break&lt;/code&gt;; the engine does not try to resynchronize because a corrupt length means the stream is no longer interpretable, and the only honest move is to stop. A truncated header is indistinguishable from an error and also breaks. There's no "half a frame" state here. When the loop exits, one line decides whether that exit was a death or a shutdown I asked for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_connected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;notify_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ConnectionLost&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;exchange&lt;/code&gt; is the anti-double-notify. If a clean &lt;code&gt;stop()&lt;/code&gt; already set the flag to false, the exchange returns false, and the error handler never fires. It's the difference between "it died" and "I killed it," encoded in a single atomic swap.&lt;/p&gt;

&lt;p&gt;The second protocol, I call it ZeriWire, carries the engine-to-runtime conversation, and it's a different animal because the far end is a different kind of fragile. It's a five-byte header (length plus a message-type byte, no channel, no flags) over the child process's stdin and stdout. And unlike the first protocol, its decoder is an incremental state machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;READ_PAYLOAD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;need&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m_payloadLen&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;m_payloadPos&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;take&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;need&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m_payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;m_payloadPos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ptr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;m_payloadPos&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m_payloadPos&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;m_payloadLen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// payload split across two reads&lt;/span&gt;

    &lt;span class="n"&gt;ZeriFrame&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;m_frameType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m_payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;ResetState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;SavePending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                &lt;span class="c1"&gt;// bytes belonging to the next frame&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why the extra machinery? Because the first protocol reads from a socket where a blocking read fills the whole buffer or fails. ZeriWire reads from an anonymous pipe, where a read hands you an arbitrary 4096-byte chunk that has no respect for message boundaries. A frame can be split across two reads; two frames can arrive in one. The state machine is the price of that. And it needs no channels (the message type &lt;em&gt;is&lt;/em&gt; the discipline), no version handshake, no PID check, because the engine launched this child itself. The first protocol exchanges a magic number and a version because its peer connected from outside and has to prove it's the right kind of stranger. ZeriWire's only handshake is a &lt;code&gt;READY&lt;/code&gt; event that means "the bootstrap loaded," not "you are who you claim to be."&lt;/p&gt;

&lt;p&gt;That's the whole reason for two protocols. A UI that connects is one contract. A child who can die mid-sentence is another. The protocols are shaped like the things they distrust.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fza1j1ehweo3xrww9wc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fza1j1ehweo3xrww9wc.png" alt="Diagram comparing two frame headers byte by byte. Protocol 1 (UI to engine) uses 6 bytes: a 4-byte little-endian length, one channel byte, one reserved flags byte, then a JSON payload. ZeriWire (engine to runtime) uses 5 bytes: the same 4-byte length, one type byte, then the payload. No channel, no flags, because the engine launched the child itself." width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sidecars: every runtime is a process that can burn down alone
&lt;/h2&gt;

&lt;p&gt;Each language runs as its own sidecar process. Python, JavaScript and TypeScript through Bun, Ruby, and Lua through LuaJIT, four isolated processes, each with its own memory and its own lifecycle. The engine is the pass: it calls the orders, times them, and handles the failures. Every station is a process, not a thread, and that distinction is the entire safety model.&lt;/p&gt;

&lt;p&gt;The runtimes aren't bundled. They're discovered on PATH at startup, four lines, one per language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;addRuntime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"lua"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"luajit"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"luajit"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;addRuntime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"python3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"python"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"python3"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;addRuntime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"bun"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="n"&gt;addRuntime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ruby"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"ruby"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No hardcoded paths, no vendored binaries — just names to look up. If a runtime isn't there, that language is marked unavailable and the rest carry on.&lt;/p&gt;

&lt;p&gt;The "each station burns down alone" guarantee isn't a comment in the code; it's a system call. On POSIX, the engine forks and execs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;pid_t&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="cp"&gt;#ifdef __linux__
&lt;/span&gt;    &lt;span class="n"&gt;prctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PR_SET_PDEATHSIG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SIGTERM&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getppid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;_exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;    &lt;span class="n"&gt;dup2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outWrite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;STDOUT_FILENO&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;dup2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inRead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;STDIN_FILENO&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ... (omitted: close inherited fds, build argv)&lt;/span&gt;
    &lt;span class="n"&gt;execvp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c_str&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;_exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A SIGSEGV in Python3 is a SIGSEGV in Python3, full stop, the engine's address space isn't reachable from the sidecar, because the only channel between them is a pair of anonymous pipes. There's no &lt;code&gt;dlopen("libpython")&lt;/code&gt; anywhere; nothing is loaded in-process. And the runtime can't outlive its parent: &lt;code&gt;PR_SET_PDEATHSIG&lt;/code&gt; asks the OS to send the child a signal when the parent dies, while the &lt;code&gt;getppid() == 1&lt;/code&gt; check closes the race window where the parent dies between the fork and the death-signal setup. On macOS, the same result comes from a small daemon watching the parent for exit; on Windows, a job object with kill-on-close is created, where the child is created suspended and assigned to the job before it's ever allowed to run. Three platforms, one promise, no orphaned runtimes, ever.&lt;/p&gt;

&lt;p&gt;Supervision is layered, and the timeouts are ordered on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kr"&gt;inline&lt;/span&gt; &lt;span class="k"&gt;constexpr&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;chrono&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="n"&gt;kLaunchHandshakeTimeout&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kr"&gt;inline&lt;/span&gt; &lt;span class="k"&gt;constexpr&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;chrono&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="n"&gt;kWatchdogTimeout&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kr"&gt;inline&lt;/span&gt; &lt;span class="k"&gt;constexpr&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;chrono&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="n"&gt;kExecuteCompletionTimeout&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The outer timeout (35s) is strictly larger than the watchdog (30s). That ordering is the design; the outer layer only wakes up if the inner safety net has already failed. It's the safety net for the safety net. And when the watchdog does fire, it doesn't report an absence; it manufactures a synthetic result and hands it to the caller, so the layer above never has to know the answer simply never came. Death gets translated into a value.&lt;/p&gt;

&lt;p&gt;There's an honest wrinkle here I'll own rather than hide. When a sidecar dies, it actually tries to say so; the Python bootstrap catches its own top-level exception and sends a CRASHED event on the way out. The engine, in this version, doesn't listen; the dispatch for that event only looks for READY, and everything else is dropped, which a comment in the code cheerfully admits. So the sidecar shouts, nobody's listening, and the watchdog quietly cleans up thirty seconds later. It works. It's not elegant. It's on the list.&lt;/p&gt;

&lt;p&gt;And when a launch fails entirely, the engine degrades instead of erroring out; it falls back to one-shot execution, running the code in a fresh, short-lived process. You lose the persistent namespace for that runtime, not the functionality. A dead sidecar gets recreated on the next command, transparently, and the only cost to you is the variables that lived in that one process. Only that one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fur44v4boptjzdszyg73e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fur44v4boptjzdszyg73e.png" alt="Diagram: the C++ engine as a central hub holding the canonical value store, connected by a separate pipe pair to each of four isolated sidecar processes, Python, JavaScript/TypeScript on Bun, Ruby, and Lua on LuaJIT. There are no connections between the sidecars; every value crosses through the engine." width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  SharedScope: the hub that owns the neutral copy
&lt;/h2&gt;

&lt;p&gt;Back to the four cooks. The value that has to move between them can't live in any one language, so it lives in the engine, in a single canonical form that belongs to none of them. The engine is the hub. Each runtime is a spoke with its own namespace, and the spokes never touch each other; the only way a value gets from Python to Bun is through the engine. The store itself is unremarkable to look at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AnyValue&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;m_sharedVariables&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;mutable&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;shared_mutex&lt;/span&gt; &lt;span class="n"&gt;m_sharedMutex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting work is the translation at the boundary, and it happens in the engine, not in each sidecar. When a value comes in as JSON, it's normalized into a neutral representation, and the rules are readable line by line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_boolean&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;AnyValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_number_integer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;AnyValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="kt"&gt;int64_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_number_float&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;AnyValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;         &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;AnyValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_array&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AnyValue&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;converted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonToAnyValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// recursion&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;converted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has_value&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;nullopt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                        &lt;span class="c1"&gt;// one bad leaf&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;                                               &lt;span class="c1"&gt;// invalidates the whole container&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push_back&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;converted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;AnyValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// ... (omitted: object branch, same recursive shape)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every integer collapses to a 64-bit int, every float to a double, strings stay strings, and arrays and maps are rebuilt recursively. The canonical form is a value that can hold exactly seven things: null, bool, int64, double, string, array-of-those, map-of-those. Nothing else can be constructed. And that recursive &lt;code&gt;return std::nullopt&lt;/code&gt; is the property worth staring at: a dictionary with a nested array survives the trip if and only if every leaf inside it is representable. One member that isn't collapses the whole container to nothing. There's no partial success, no "I saved the parts I could." Either the value crosses the whole, or it doesn't cross.&lt;/p&gt;

&lt;p&gt;Two decisions shaped this, and both are the "assume death" rule wearing different clothes.&lt;/p&gt;

&lt;p&gt;The first is copy-by-value, not shared memory. &lt;code&gt;GetShared&lt;/code&gt; returns by value; no overload hands back a reference, no &lt;code&gt;GetSharedRef&lt;/code&gt;, nothing that could be mistaken for a live pointer into the hub. This is deliberate to the point of stubbornness. Four processes, four different garbage collectors, or in LuaJIT's case, its own, and a live reference across that boundary would be a promise none of the four could keep. The copy is the discipline. Pretending four separate processes share memory would be a lie engineered to crash later, so the system doesn't pretend.&lt;/p&gt;

&lt;p&gt;The second is loud failure. If a value doesn't fit the supported set, the engine doesn't mangle it into something that looks fine; it refuses, and the set fails on the spot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;converted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DeserializeAnyValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;converted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has_value&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;runtimeState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetShared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;converted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Unsupported value type."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That error rides back to the user's code and becomes an exception on the exact line that called set, not three steps later, wearing a disguise, in some unrelated function. Break loudly beats deliver-corrupt-quietly.&lt;/p&gt;

&lt;p&gt;Except, and this is one of the lines I had to fix while writing this article, the rule had a hole in it, and the hole said the opposite of what the rule promised. The integer branch did &lt;code&gt;value.get&amp;lt;std::int64_t&amp;gt;()&lt;/code&gt; with no range check. An unsigned integer above int64 max would get silently truncated, and one bigger still would quietly degrade to a float. It was the single place in the whole system that would hand you the wrong value without a word, the exact betrayal the loud-failure rule exists to prevent, sitting right in the middle of the mechanism that's supposed to enforce it. I found it setting a big number in Python and reading a different number back in JavaScript. The fix is boring: an out-of-range integer is now refused the same way a BigInt is, returns nothing, and raises on the line. The canonical set is still seven types. The rule now actually holds.&lt;/p&gt;

&lt;p&gt;One last detail I like, because it shows the same data can warrant two different policies. When a shared value can't be serialized, the machine-facing list skips it silently, a lost entry, no trace. But the human-facing &lt;code&gt;/shared list&lt;/code&gt; command shows it, marked &lt;code&gt;&amp;lt;unsupported&amp;gt;&lt;/code&gt;. Same underlying value, two audiences, two honest answers: the machine gets a clean list it can consume, the human gets the truth that something's wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI layer, and where it deliberately isn't
&lt;/h2&gt;

&lt;p&gt;Zeri has a local-LLM feature; you can talk to a model from inside the REPL. It's worth thirty seconds here purely because of where it lives, which is a clean example of the engine/frontend boundary holding.&lt;/p&gt;

&lt;p&gt;The whole thing lives in the Go layer. The TUI builds an OpenAI-compatible request, streams the response back as UI events, and the C++ engine never sees a token, a prompt, or a reply.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequestWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodPost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;"/v1/chat/completions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's one honest caveat. The engine isn't totally ignorant of the feature: it stores and serves the AI settings, endpoint, model and key, because the engine owns persistence and the UI has no store of its own. So the division is precise, the UI knows how to talk to the model, and the engine knows where the keys are. The engine holds the configuration and never once uses it. It's the tidiest possible version of "not my job."&lt;/p&gt;

&lt;h2&gt;
  
  
  Yuumi, and why the protocol got its own name
&lt;/h2&gt;

&lt;p&gt;The UI-to-engine protocol, the framing, the transport, the handshake, the bridge, is factored out into its own thing, called Yuumi. Inside the repo, it's a separate build target on the C++ side and a separate package on the Go side, with one property that matters, the dependencies only point one way. Nothing under Yuumi includes anything from Zeri. Yuumi does not know Zeri exists; it transports and it doesn't understand. The bridge hands up a raw, uninterpreted map and lets the layer above decide what any of it means.&lt;/p&gt;

&lt;p&gt;That discipline, the seam being real, not aspirational, is what later made it possible to spin the idea out into a project of its own. I'm not going to oversell that here. The point for this article is narrower and more useful: drawing a boundary strictly enough that one side genuinely can't reach into the other is what lets you eventually lift that side out. The isolation wasn't for show. It was the thing that made the piece separable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one idea, again
&lt;/h2&gt;

&lt;p&gt;If you've read this far, you've seen the same decision made in five different places. The two-process split contains a crash. The two protocols each distrust a different kind of peer. The sidecars burn down alone. Copy-by-value refuses to fake a shared memory that four processes couldn't honor. Loud failure refuses to hand you a value it isn't sure about, and where it quietly did anyway, that was a bug, and now it doesn't.&lt;/p&gt;

&lt;p&gt;None of this makes Zeri important. It's still a REPL that went looking for a problem architecture-first, and the companion article is honest about what that cost. But the architecture itself has one spine, and the spine is a single sentence: assume the other side can die, and design the seam so that when it does, nothing lies to you about it. Everything else in this system is a consequence of taking that seriously.&lt;/p&gt;

&lt;p&gt;If you want the why, why build this before anyone asked, that's the other article. This was how. If you want more information about the project or some aspects of the code in particular, I am open to further explanations.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>architecture</category>
      <category>showdev</category>
      <category>systems</category>
    </item>
  </channel>
</rss>
