June 19, 2026

Turns become a chain, not a loop

A ground-up rewrite of how Chalie processes a conversation turn, plus the multi-device sync and packaging work needed to actually ship it.

Today was about ripping out the core loop at the center of every Chalie conversation: many tool calls, many bubbles, one turn.

The old model treated a “turn” as a while-loop: keep calling tools until the model stops and hands back one final message. It worked, but it lied about what was happening — a turn that fired six tools across three reasoning passes still pretended to be one atomic unit by the time it hit the database. So I rebuilt it as a recursive chain. Each step that emits tool calls writes its own row, anchoring the calls it made, dispatches them, blocks until they’re done, then spawns a continuation that rebuilds its state from the database and recurses. A step with no tool calls is where the turn ends. No iteration cap — depth is bounded only by when the model stops reaching for tools, and a runaway is a hard-restart condition, not a hand-tuned ceiling. A follow-up pass deleted everything the old loop needed that the chain doesn’t: state carried by reference between steps, an unused metrics accumulator, three stale test suites built on the loop’s assumptions. Net effect: about 1,300 fewer lines, and a MessageProcessor that’s genuinely legible.

That kind of rewrite only earns its keep if the surface people look at tells the truth about it, so the frontend got a matching rebuild. Each chain step renders live as its own bubble with tool activity nested underneath; when superseded, that group collapses to a clean summary line instead of sitting there expanded forever. It sounds cosmetic until you’ve seen the alternative — before a same-day fix landed, any tool-only step emitted nothing on the wire, so the previous group never collapsed and a long turn rendered as one ever-growing wall of expanded calls. That’s exactly what makes a product feel unfinished even when the logic underneath is sound, so it got a regression test driving the real broadcast path end to end.

Multi-surface sync got fixed the same day: Chalie is single-user, but that user opens it on a laptop, a phone, and a second tab, and all three need to agree on what’s happening. The broker used to hold one connection reference, so the newest tab silently stole every broadcast. It now fans broadcasts to every live connection and prunes anything that fails to send, so closing one tab doesn’t black out the rest. Messages carry a client-minted echo id so the sender doesn’t double-render its own message coming back over the wire.

The rest of the day was the unglamorous work that makes a rewrite shippable. The frontend’s compiled output is now committed to the repo, so a plain source install gets a working UI without Node or a build step — which let the Docker image drop its frontend-builder stage entirely, since it was rebuilding what the runtime already had. I also caught a packaging bug in the skills pipeline: the shipped skill library is meant to hold only curated, reviewed skills, but a rebuild script had let three personal skills leak in. That’s now structurally impossible. And a long-standing memory bug closed out: recalled memories were hard-truncated to 200, even 160, characters before the model saw them. That cap is gone — what gets recalled is now what gets read.

None of this is a feature you’d screenshot, but it’s the gap between a product that scales its complexity gracefully and one that rots under its own ambitions. Next test: whether it holds up once turns branch into background delegates and scheduled work.

  • MessageProcessor rewritten as a recursive turn chain (no iteration cap, ~1,300 fewer lines) replacing the old while-loop model

  • Frontend renders each turn step as its own live bubble with collapsible tool groups; a same-day fix closed a bug where tool-only steps never collapsed the prior group

  • WebSocket broadcasts now fan out to every open device/tab instead of only the most recently connected one

  • Compiled frontend assets are committed to the repo so source installs and the dev container ship a working UI without a Node build step

  • Skill packaging locked to curated-only, and memory recall no longer truncates recalled context to 160-200 characters