Threads

How a reply becomes a thread — the turn model, auto-generated labels, per-thread context and thinking mode, and the REST surface that drives it.

A conversation with Chalie isn’t flat. Ask something, get an answer, and that exchange is a turn — a group of transcript rows that share one id. Reply into that answer instead of starting something new, and the turn becomes a thread: a branch that carries its own back-and-forth, its own context budget, and its own thinking setting, without disturbing the main conversation. This page covers how threads work under the hood — the turn model, the auto-label, thread-scoped context, and the REST surface the client drives it through. For the turn lifecycle itself, see Message Flow; for how threads fit into memory, see Memory & Cognition.

The turn model

Every reply Chalie writes belongs to a turn, and every turn has an id. A first exchange — your message, Chalie’s answer — is one turn: one opening message plus whatever it takes to produce a settled reply. The moment a second message lands on that same turn, the turn has grown into a thread: everything from that second message onward is the thread’s private continuation, while the main conversation still only shows the original exchange.

This means “thread” isn’t a separate object you create — it’s a state a turn reaches. A turn that never gets a reply stays a single exchange on the main spine forever; a turn that gets replied into becomes browsable as its own branch, indefinitely, for as many further replies as you send it.

The auto-label

A raw turn id isn’t something you want to read in a list. The first time a turn becomes a thread — the moment its first reply lands — a small, silent side-call reads just the opening message and the first answer and writes a terse three-to-five-word topical label for it. It fires exactly once per thread, never again on later replies, and it costs nothing on the visible turn: it runs as its own background call, off to the side of whatever you’re doing.

When the label is ready, it’s pushed live: the conversation list gets a signal telling it that thread’s collapsed view is stale, refetches it, and the thread appears with its short title with no page refresh and no polling.

Thread-scoped context

Open a thread and you see the whole thing — every reply, unfloored, in full. That full exchange is scoped to the thread: it’s available the moment you’re inside it, but it never leaks onto the main conversation’s view, which continues to show only the original opening exchange.

Compaction — the mechanism that summarizes an aging conversation so it keeps fitting in the model’s context window — follows the same split. It runs on two independent axes: the main spine keeps its own compaction checkpoint, and every thread keeps its own, separate checkpoint. Summarizing a long thread never touches the main spine’s checkpoint, and summarizing the main spine never touches any thread’s. Each surface manages its own context budget entirely on its own.

Per-thread thinking mode

Deliberation effort — how hard Chalie thinks before answering — is normally decided automatically, turn by turn. A thread can override that for itself: set it to medium or high and every reply in that thread runs with that override as a hard floor, regardless of what the automatic classifier would have picked. Set it back to auto and the thread returns to the same automatic gate as everything else.

The setting lives on the thread, not globally — one thread can run on auto while another sits pinned to high, and each one remembers its own choice independent of the others.

Per-thread context usage

The “how big was the last request” indicator that shows context pressure can be scoped the same way. By default it reports the main spine’s most recent call, but it can be asked for a specific thread’s own most recent call instead — so a thread that’s grown long and a fresh main conversation each show their own real number, not a blended one.

Threads are keyword-searchable. A search term matches a thread when it appears in any of your own messages inside that thread — the opener or a later reply — or in the thread’s auto-generated label. The match is case-insensitive and works against the same feed the collapsed thread list renders from, so search is just a filtered view of the same data, not a separate index.

Lanes: independent surfaces

The main conversation and every open thread are treated as independent surfaces — internally called lanes. Each one tracks its own busy/idle state on its own. The main spine being mid-answer has no bearing on whether a given thread is free to receive a message, and vice versa: you can have the main conversation working on something while you carry on a completely separate exchange inside a thread.

The message queue

If a lane is busy — a reply is still being generated — anything else typed into that same lane doesn’t interrupt it and doesn’t get lost. It queues, scoped to that lane. The moment the in-flight turn on that lane finishes, every queued message is joined into one and sent as a single follow-up. This is what makes it safe to keep typing into a thread that’s still thinking: your follow-ups land in order, together, the instant it’s free, without ever landing mid-generation.

The REST surface

Threads are driven entirely over HTTP; there’s no dedicated push channel for sending into one. Four endpoints cover the whole surface:

GET    /api/threads            the thread feed — supports ?q= for search
GET    /api/thread/<turn_id>   one thread's full block
POST   /api/thread/<turn_id>   send — -1 starts a new thread, a real id replies into it
DELETE /api/thread/<turn_id>   interrupt the thread's in-flight turn

POST /api/thread is the single send chokepoint for both the main conversation and every thread — sending -1 opens a brand-new turn, and sending an existing id replies into that turn, which is what turns it into a thread on the second such call. This replaced the older chat-ingest endpoint entirely; live output during generation still arrives over the WebSocket described in Message Flow, with the REST calls above used to send and to read state back.

  • Message Flow — the turn lifecycle every thread reply runs through, and how live output reaches the client.
  • Memory & Cognition — how a thread’s content is recalled and folded into later turns.
  • Ambient Awareness — the background cognition loop that runs independently of any open thread.