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.
- 01Your app sends a messageRequest
POST /threads/:id/messages - 02The runtime accepts and records itDurable
202 · seq · ts - 03An engine executes the turnRunning
Model → tool → result → model
- 04Events reach your interfaceStreaming
Live deltas for responsiveness; stored events for recovery.
- 05Your app reads the outcomeSettled
A completed or failed turn is distinct from acceptance.
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.
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.
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, 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": trueto keep it behind the running turn instead (a held message cannot also carrytoorfrom). The answer names a held row. Until the message is delivered, you can edit it withPATCH /threads/:id/messages/:seqor withdraw it withDELETE /threads/:id/messages/:seq. - Stop it.
POST /threads/:id/interruptstops 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.