June 30, 2026

Scheduled threads get their own stable spine

Scheduled conversations now settle into one durable thread instead of scattering across turns, and the interface's motion and color system gets scrubbed down to a single set of tokens.

When a schedule fires — a reminder, a recurring check-in, anything Chalie runs on its own clock instead of because you typed something — where does that conversation live? Up to today the answer was “it depends.” Not something you want in a memory system.

Fixed it with a settled spine. Every turn now carries a settled flag. Tool steps that are just Chalie working — searching, checking a calendar, drafting before it answers — get demoted to unsettled. The thread resolves to the first settled assistant row, not the last row written. That’s the difference between a thread showing you the answer and one showing scratch work because a tool call happened to finish last. On top of that: one schedule now fires into one stable turn_id instead of minting a new one every run, backed by a dedicated scheduled-turn config and a per-source profile. A recurring reminder is now a single growing thread you can scroll back through, not disconnected turns each restarting the story from zero. Continuity instead of a notification log.

The frontend had to catch up to that. Killed the Pinia conversation store — one global blob every part of the UI read from — and replaced it with a channel-keyed feed factory. REST is now the only source of data. The WebSocket only drives visual signals, things like “this is working right now.” A socket hiccup can’t show stale data anymore, because the socket never holds data to begin with. The scheduler dock got a real home too: a clock button opens a slide-out of active schedule threads with per-row activity blinkers, and every reply carries its channel end to end, so a queued send drains onto the schedule thread it belongs to instead of leaking into your main conversation. That kind of leak is exactly what erodes trust in a memory system.

Rest of the day was scrub work. Six ad-hoc spin animations — history, the thread panel, voice recording, the voice player, action cards, the update banner — collapsed into one canonical keyframe. Same look, one definition, five fewer things to keep in sync. Hardcoded colors across focus rings, ambient blooms, connection and capability banners, the escalation warning, and the voice spinner moved onto theme tokens, so dark and light can’t drift apart by accident. Thread panel picked up a frosted rose scrim while I was in there — small, but it makes threads feel like their own space instead of a modal bolted onto the main feed.

Two more things got deleted, not patched: a stale test seeding a transcript role that doesn’t exist anymore, and a “created” turn-state signal the processor stopped emitting, still lingering in docstrings pretending it was real. Neither changes behavior. Both stop the docs lying about the code, which is its own kind of bug.

Scheduled agents can’t run unattended until “did this happen” and “where does it belong” each have exactly one answer. That’s what today was actually for.

  • Added a settled flag on transcript rows so a thread resolves to the first settled assistant answer, not whichever tool step wrote last

  • Schedules now fire into one stable, growing thread per schedule instead of a fresh turn on every run

  • Replaced the global Pinia conversation store with a channel-keyed feed factory: REST is the only data source, WebSocket frames are visual signals only

  • Scheduler dock added — a clock button opens a slide-out of active schedule threads with live per-row activity indicators, and queued replies now drain on the correct channel

  • Consolidated six duplicate spin animations into one, and scrubbed hardcoded colors across banners, focus rings, and the voice spinner onto theme tokens for full dark/light parity