# Threads and the runtime (https://condensate.dev/concepts/runtime/)

> Send messages, read events, survive restarts and coordinate threads.

This page covers the lifecycle of a thread: sending a message, reading the result, recovering after a restart and coordinating several tasks.

A thread is the durable record of a task. It holds the messages and events that let an application show what happened, recover after a disconnect, and continue the conversation. An engine session does the work. Condensate has three engines: Claude Code, Codex, and Condensate's own agent. Because the thread is separate from the session, an application can follow a task after a browser connection closes.

[Diagram: One message, from acceptance to result. 202 acknowledges the message. The later turn outcome tells you whether the work succeeded.]

## Send a message

Start with an existing thread on your running runtime. To create one, call `POST /spawn`, which answers with a `threadId`. The runtime listens on `127.0.0.1:4741` by default; set `CONDENSATE_AGENTS_PORT` to change the port.

Replace `THREAD_ID` with the thread's ID. This example sends real work to that thread and can consume model usage.

```sh
export THREAD_ID='replace-with-an-existing-thread-id'

curl -i "http://127.0.0.1:4741/threads/$THREAD_ID/messages" \
  -H 'content-type: application/json' \
  -d '{"text":"Summarize the current task and its next step."}'
```

The runtime acknowledges an accepted message with HTTP **202** and a JSON body containing `seq` and `ts`. Save the sequence number. Acceptance means the message was recorded before any model ran. The turn result, read from the log, tells you whether execution succeeded.

While the runtime is draining, for example during a restart, this route answers **503** with the code `draining`. Nothing is recorded in that case, so retry once the runtime is open again.

## Read the result

Use the acknowledged sequence number as `AFTER`. A forward tail reads the events that come after that position.

```sh
export AFTER='replace-with-the-acknowledged-sequence-number'

curl -fsS \
  "http://127.0.0.1:4741/threads/$THREAD_ID/tail?after=$AFTER&limit=20"

curl -fsS \
  "http://127.0.0.1:4741/threads/$THREAD_ID/turn"
```

A forward tail page contains `events`, `nextAfter`, and `hasMore`. Continue with `nextAfter` to read the next page. The default page size is 200 events, and the maximum is 1000. Add `wait=<milliseconds>` to hold an empty page open until the thread moves past your cursor, for up to 30 seconds. A read from sequence 0 can also include a `snapshot` that stands in for the older events, and `events` then starts after it. Add `history=1` to read the full event history behind the snapshot instead.

`GET /threads/:id/turn` reports whether a turn is running (`active`), which messages are still queued, and whether the thread is `degraded`.

To open a long thread at its end, read the reverse page with `before`. Omit `before`, or leave it empty, to start from the newest event. The page returns events newest first, plus `prevBefore` and `hasMore` for the next older page.

To keep a connection open instead of polling, use `GET /threads/:id/follow?after=<seq>`. It streams events as server-sent events. The `Last-Event-ID` header works as the cursor too. The [SDK](https://condensate.dev/guides/sdk/), which ships with early access, provides a follower (`agents.follow`) that manages reconnects and falls back to polling where streaming is unavailable.

## Build a responsive interface

Use the asynchronous message endpoint when your UI should acknowledge a send immediately. Render live deltas while the turn is running, then reconcile against stored events. Keep the latest durable cursor so a reconnect can pick up after it.

| Surface | Route | Best use |
| --- | --- | --- |
| Thread summaries | `GET /threads` | A task list or sidebar |
| Durable event pages | `GET /threads/:id/tail` | History, recovery, and recorded outcomes |
| Live deltas | `GET /threads/:id/stream` | Showing an answer as it is generated |
| Turn status | `GET /threads/:id/turn` | Understanding the current execution state |

Live deltas have no replay and no sequence numbers. Treat them as a preview, and read the settled text from the tail or follow streams.

The `/send` endpoint offers a different contract. It holds the request until the turn settles and returns the reply. Use it when your caller needs a single awaited answer. Send `"async": true` to get the same 202 acknowledgment as `/messages`.

Two controls apply when a message arrives during a running turn:

- **Hold it.** By default, a message posted during a running turn is steered into that turn. Post with `"hold": true` to keep it behind the running turn instead (a held message cannot also carry `to` or `from`). The answer names a held row. Until the message is delivered, you can edit it with `PATCH /threads/:id/messages/:seq` or withdraw it with `DELETE /threads/:id/messages/:seq`.
- **Stop it.** `POST /threads/:id/interrupt` stops the running turn, drops queued messages, and cancels any pending retry.

## What survives a restart

Thread records and ordered events are stored in SQLite, in `threads.db` under `~/.condensate/agents-host` by default (set `CONDENSATE_AGENTS_DATA_DIR` to move it). When a thread is compacted, its older event bodies move to `threads-archive.db` in the same directory. Event bodies larger than 64 KiB (the default; `CONDENSATE_AGENTS_BLOB_THRESHOLD` changes it) are written to content-addressed files in the `blobs/` directory, and the database keeps only their hash. Engine resume pointers live in the engine state directory, `~/.condensate/engine` by default.

Back up `threads.db`, `threads-archive.db`, the `blobs/` directory, and the engine state together when you move a deployment. Without the engine state, engine sessions cannot resume their provider conversations.

A durable record lets an application recover recorded history. Resuming execution also depends on the engine's resume state and on provider behavior. A restart stops any turn that is running, and messages waiting behind it do not run on their own. The messages stay in the log. At boot the runtime writes an error message to each affected thread, starting "This turn was lost to a host restart", so you can resume or send again. Held messages and scheduled retries are stored as rows, so they survive the restart and run afterwards. Engine sessions are recreated on the next turn.

Archiving a thread (`DELETE /threads/:id`) marks it archived. `POST /threads/:id/restore` brings it back.

## Coordinate several tasks

The runtime exposes child threads, subscriptions, approvals, interruption, and handoffs. A child has its own history, so its work can be inspected independently. Coordination records relate that work to its parent.

| Need | Route |
| --- | --- |
| Start a child thread under a parent | `POST /spawn` with `parent: { threadId, atSeq?, forSeq? }` |
| Fork a thread at a point in its history | `POST /threads/:id/fork` |
| Watch another thread and be woken by its events | `POST /threads/:id/subscriptions` (up to eight per thread by default; `CONDENSATE_AGENTS_MAX_SUBSCRIPTIONS` changes the cap) |
| Allow or deny a pending approval | `POST /threads/:id/approvals/:approvalId` with `decision: "allow"` or `"deny"` |
| Hand work to another thread | `POST /threads/:id/handoff`, then accept or decline it |

Start with one thread and its event stream before building a coordinator. The same read-and-follow pattern applies to every thread.
