Persistence (Advanced)
- Overview
- Quick Start
- Providers
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
Message Queue
The user hits send while the reply is in flight. You need a rule for that message: wait, ignore, or cut in.
By default the message waits in queue. It sends after the current run succeeds.
1. Show the pending queue#
useChat exposes queue separately from messages. cancelQueued(id) drops an item before it sends:
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
const { messages, queue, sendMessage, cancelQueued } = useChat({
connection: fetchServerSentEvents("/api/chat"),
});function PendingQueue() {
return (
<>
{queue.map((item) => (
<div key={item.id} className="pending">
{typeof item.content === "string" ? item.content : "[attachment]"}
<button onClick={() => cancelQueued(item.id)}>Cancel</button>
</div>
))}
</>
);
}Render queue with a different style from messages. Once the text appears in queue or messages, clear the composer.
2. Pick whenBusy#
Pass a queue option. A string is shorthand for { whenBusy }:
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
const { sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat"),
queue: { whenBusy: "queue", drain: "fifo", maxSize: 5 },
});whenBusy is what happens to a send that arrives while the client is busy. The client is busy when a stream is active, a send is in flight, or the queue is draining:
- "queue" (default): hold the message. It sends once the run settles successfully.
- "drop": ignore the send. The promise still resolves. The message never appears in queue or messages. Keep the composer text and show feedback if you want a retry.
- "interrupt": abort the current stream and send the new message now. Already-queued messages stay. They drain after the interrupting send succeeds.
Override the policy for one send with the second argument to sendMessage:
sendMessage("Never mind, do this instead", {
whenBusy: "interrupt",
body: { source: "composer" },
});body is extra JSON for that request only. It is merged into forwardedProps.
Later#
- drain: "fifo" (default) sends queued items one at a time in order. "batch" merges the queue into one send after a successful settle (strings join with \n, multimodal parts stay in order, last body wins).
- maxSize: caps how many messages can wait (0 means never queue).
- onOverflow: "reject" (default) ignores a send once maxSize is reached. "drop-oldest" evicts the oldest queued item to make room.
- Strategy function: pass a function as queue for per-send control. That form always drains FIFO. Per-call whenBusy still wins.
When the queue drains versus flushes#
- Drain (auto-send): only after a successful stream settle, including after tool continuations finish.
- Flush (discard without sending): on error or abort of the active generation (stop(), real stream errors), clear(), unsubscribe(), and reload().
- interrupt does not flush: existing queued items remain. They drain after the interrupting turn succeeds.
The composer no longer fights the stream. A send while busy waits, drops, or cuts in, on purpose.