Isolated per thread
Queues are isolated per thread: one FIFO per
threadId, and one serial chain per
thread. Turns in different threads run in parallel and never block each other.agent.prompt(), it
hit the Agent is already processing guard. Now it enters that thread’s FIFO and is
executed in order by that thread’s serial chain.
So “the queue” is not one global box — it is one queue per conversation. Queueing
in session A never blocks session B.
The four actions
- No pause: a dispatched item has already left the snapshot, so there is no locked state to speak of; when the previous turn wraps up the chain automatically takes the head of the queue and runs
- No circuit breaker: a failed item is reflowed into a bubble when the stream ends, and whether to retry is your call — not an automatic gate deciding that the next few should stop too
Where the earlier problems came from
Queue state was not persisted, the source of truth was scattered, and the frontend
performed multiple surgeries on the message array. After a refresh or restart,
queued messages vanished and the queue bar turned into a ghost; the message array
needed extract/refill/restore/suppress paths, plus recovery races, duplicate
reconciliation appends, and ordering churn; any side stream finishing reset the
session status to ready, making the button flicker.
The queue was rewritten as a pure state machine,
QueueEngine — queue, auto-increment
id, and dispatch settlement all live in one class with no I/O. Every change
appends a full-snapshot line to the session transcript, so it can be restored after a
restart, a thread switch, or tree navigation; the same full snapshot is broadcast to
the thread, where the frontend applies “last snapshot wins”.The reason for full snapshots over deltas is plain: a queue rarely exceeds five items,
so full is the simplest option, has no reconciliation logic, and broadcasts rarely.
How a snapshot travels
- Persisted: every change appends a
queue_statefull-snapshot line to the session JSONL - Restored: after a sidecar restart it is replayed via
get_queue_state, and it no longer auto-pauses - Broadcast: every change sends
data-queue-stateto that thread viasendEventChunk - Conflict: the frontend applies “last snapshot wins”. Snapshots are dropped silently when the thread has no active request — idle-state changes are initiated by the frontend’s own invoke, so the frontend updates itself from the reply
The “same id updated in place across phases” incremental chunk shape has been
removed entirely. Enqueue, position, and dispatch are all carried by full
snapshots; there are no per-item lifecycle chunks.
Three frontend sync rules
Every operation the frontend performs on the message array collapses to three idempotent rules:
Retired paths — restore race compensation, cancelled flags, direction branching in
reconciliation — are all replaced by “snapshot state plus three rules”.
Stable ids
Items carry a persisted auto-increment id that never repeats across restarts, and both dispatch and cancellation address items by that id. This is not a detail: addressing by array index meant that after a restart an index mismatch would cancel the wrong item.Attachments survive
Early snapshots persisted text only, so bubbles rebuilt after a refresh lost their attachments. That was fixed — snapshot entries now carry image attachments (protocol shape{ name, mimeType, data | path }, identical to a prompt frame), so restore after
a refresh or restart, relay-pump re-dispatch, and queue bar thumbnails all keep the
image.
There is a trap worth remembering here: the sidecar originally inferred the shape from
a
type: "image" field the frontend never sent, so it silently dropped every
image — meaning no historical queue_state line carries an attachment, and nothing
errored. The lesson: align protocol shapes with the ones actually in use rather than
inventing a new field.Known boundary
Refreshing between “sent” and “confirmed”: the registry lives in frontend memory, so a refresh clears it. The item is then rebuilt from the snapshot (attachments included), which is acceptable.Next
Modes overview
Five different “modes” in one app — do not conflate them.
Conversation workspace
Where the queue bar sits in the conversation stream.
