Skip to main content
The prompt queue lets you type follow-up instructions while the agent is still working. There are only four actions on the queue bar, and that is deliberate. This page covers what each one does, plus the one design decision behind them: queue state is a full snapshot, not a delta. It happens to be a good illustration of how Kova handles frontend state in general.

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.
That isolation fixes the messiest class of early problems: when a new prompt arrived while the previous turn was still running and went straight to 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

Editing, pause/resume, and failure circuit-breaking were removed on purpose during the refactor — they are not oversights:
  • 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_state full-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-state to that thread via sendEventChunk
  • 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.