June 27, 2026

One route convention, one source of truth for a turn

The entire API surface moves onto a single routing convention, and the thread system converges on one function that decides what a conversation looks like.

Today was about collapsing two kinds of sprawl at once: inconsistent API routing, and a thread system that had grown more than one opinion about what a “turn” is. Neither is a feature you can screenshot. Both are the difference between a codebase that gets easier to build on and one that gets harder every week.

The API migration is the bigger of the two. Every HTTP route in the backend — 29 files — moves from ad-hoc Flask blueprints to a single, strict convention: one class per resource, documented inputs and outputs, one place routes are declared. It’s invisible until you’re adding the 30th endpoint and it takes ten minutes instead of an hour of guessing how the last person did it. The first pass broke things — a handful of routes shifted URLs they shouldn’t have, auth rejections started returning 500s instead of clean 401/403s, a retry header vanished — so the very next commit was a full audit restoring exact pre-migration behavior end to end, plus a permanent test helper so this class of regression can’t slide back in silently. A third pass then re-earned full strict type-checking across the whole API layer. Three commits, in order: convert, verify nothing moved, prove it’s sound. That’s the standard I want on anything touching every route in the product.

The other half of the day went into the thread system, and this one matters more directly to how Chalie feels to use. Threads — replying inline to something Chalie said turns ago without derailing the main conversation — had accumulated multiple code paths, each with its own idea of a turn’s history: one for the main feed, one for a thread panel, one for live socket updates. Multiple sources of truth for the same data is exactly how you get a thread that looks right until you refresh, or a reply that shows up twice. Today that collapsed into one function every surface calls through — REST reads, the batch loader, and the live update path all ask the same question and get the same answer. No more state that only exists in one code path’s memory.

That consolidation also fixed something users would notice: turns were quietly collapsing when they shouldn’t. The fix draws a precise line — a turn’s own answer is never collapsible, only the reply thread hanging off it is. The main conversation now always shows every turn’s full answer inline, and a “Thread” affordance only appears when a reply thread actually exists to open. Less magic disappearing, more predictable feed. Alongside it, the old thread search feature (with its own embedding index) came out — a case of complexity not earning its keep relative to what it did for the reading experience, so it’s gone rather than dragged forward half-maintained. Net effect on the thread system alone: about 1,500 fewer lines doing the same job better.

Smaller but not nothing: a synonym-registry change for how tools handle parameter names and follow-up calls, making tool use more forgiving when the model reaches for a slightly different argument name than expected — one less way a capability silently fails on a technicality.

None of this is a feature announcement. It’s the kind of day where the product doesn’t look different tomorrow, but the next ten features touching threads or API routes will ship faster, with fewer surprises. That’s the trade I’ll keep making.

  • Migrated all 29 backend API route files onto a single, strict routing convention, then fully audited and restored pre-migration URL and error behavior end to end

  • Re-established full strict type coverage across the entire API layer following the migration

  • Thread history now flows through one single function for every surface — main feed, thread panel, and live updates — eliminating drift between them

  • Fixed turns silently collapsing in the main feed: a turn’s own answer always renders in full; only its reply thread is collapsible, and only when one exists

  • Removed the old embedding-based thread search (net ~1,500 fewer lines in the thread system) and shipped a synonym-registry fix so tools tolerate near-miss parameter names