June 29, 2026

One API surface, one lane per thread

Every namespace in the REST API now speaks one typed contract, and the spine and its threads finally run as independent lanes instead of one shared queue.

Two things landed today, and the second one mattered more. First: the thread-system rebuild from yesterday finally has a finished API to sit on. Second: a busy thread no longer freezes everything else. That second one had been bugging me for days.

Start with the API. Sixteen namespaces — provider, auth, chat, system, conversation, privacy, snapshot, policies, capabilities, documents, memory, voice, personality, scheduler, skills, more — all moved onto typed request/response contracts in one push. Same @expects/@responds boundary, same normalized error codes, no exceptions. 116 routes, full type-check, 1405 tests green. Two branches had been building toward this in parallel and merged clean.

Except it wasn’t clean on the way there. A merge picked up a stale import and crash-looped the API at boot — caught fast, fixed, moved on. Worse: collection endpoints stopped returning { providers: [...] } wrapper envelopes and started returning bare arrays. The frontend was still unwrapping. Every brain settings view and the chat task drawer went silently empty. Nobody threw an error — the UI just rendered nothing and looked fine doing it. Fixed once found, but that’s the failure mode I hate most: no crash, no log line, just an empty screen that looks intentional.

Now the real fix. Up to today, the main conversation and every thread forked off it shared one lane. Send a message into a thread while the main conversation is mid-reply and they’d trip over each other. Not anymore — spine and every open thread are now independent lanes keyed by turn ID, each with its own busy/queue state. A thread streaming a reply in the background doesn’t block anything, and nothing blocks it either.

Control actions became real REST verbs to match: stop a turn, cancel a background task — both DELETE by ID now, not a POST hiding a special case inside the chat endpoint. Interrupting a turn doesn’t cascade to a delegate running under it either — cancel the turn and the delegate keeps going until you cancel it too. Small API decision, real behavior difference: it’s the gap between “the app just froze” and “one thing paused, everything else kept moving.”

Two smaller fixes rode along. A background thread’s typing indicator was leaking onto the main view — fixed, along with giving a settled reply in the activity list a proper “waiting to be seen” state instead of vanishing the moment it resolves. And I found two dead /system/update endpoints, orphaned by a service deletion from earlier, returning a 500 on every single call. Ripped out, along with the request model behind them. No idea how long those had been quietly failing — nobody was hitting them, which is its own kind of warning sign.

The API being uniform and the execution lanes being independent are the same precondition: neither matters alone, but together they’re what makes running several threads at once actually safe rather than just possible.

  • Finished the REST API migration: all 16 remaining namespaces on typed DTOs; 116 routes boot clean, 1405 tests pass

  • Fixed a merge defect crash-looping the API at boot, plus a frontend/backend contract mismatch that had silently emptied every settings view

  • Gave the spine and every open thread independent lanes — a busy thread no longer blocks the rest of the app — and made stop/cancel real REST verbs scoped to the exact turn or task

  • Stopped a background thread’s typing indicator from leaking onto the main view, and gave settled replies a proper “waiting to be seen” state instead of vanishing

  • Removed two dead system endpoints, orphaned by an earlier deletion, that had been silently erroring on every call