Files
kandev/docs/specs/ui/message-queue-send-now.md
T
2026-08-19 14:04:12 +01:00

282 lines
14 KiB
Markdown

---
status: shipped
created: 2026-08-05
owner: kandev
---
# Send Queued Messages Now
Decision:
[ADR-2026-08-05-queue-send-now-replaces-turn](../../decisions/2026-08-05-queue-send-now-replaces-turn.md)
The first-party header, Auto-run policy, and row placement are governed by
[Control Pending Message Auto-run](message-queue-run.md). This spec remains
authoritative for targeted replacement delivery and the backward-compatible
`message.queue.send_now` protocol, including `scope: "all"`.
## Why
When an agent is in a long-running turn, an urgent correction can sit in the
queue until that turn finishes. Users need to replace the current turn with one
specific queued instruction, or with the whole pending queue as one coherent
follow-up, without completing or advancing the task's workflow step.
## What
- Every visible pending queue row, including the FIFO head, offers **Send
Now**. On a fine-pointer desktop it appears with row actions on hover or
keyboard focus; on a coarse-pointer surface it is always visible and
touch-sized. No separate **Skip to next** action duplicates this behavior.
- Per-row **Send Now** interrupts the active agent turn and dispatches that
exact entry as the first prompt of a replacement turn. This behaves like a
transient promotion without persisting a reorder: other queued entries keep
their relative FIFO order. An accepted exact claim also turns Auto-run ON,
so automatic FIFO delivery continues through them after the selected turn
completes and the session is eligible.
- The first-party queue header does not offer bulk **Send Now**. Protocol
clients may still dispatch the click-time snapshot of all visible pending
entries as one replacement turn with `scope: "all"`.
- Bulk content is concatenated in FIFO order with one blank line between
non-empty message bodies. Attachment-only entries add no empty separators.
Attachments retain FIFO order, and entity references are combined in first-
occurrence order with canonical duplicates removed.
- The bulk turn uses the oldest selected entry's model and plan-mode snapshot.
Its transcript envelope uses that entry's sender attribution while retaining
source entry identities and provenance in metadata for restoration and
diagnostics.
- If the aggregate would exceed the existing per-message attachment count,
attachment byte, or entity-reference limits, the request is rejected before
cancellation. No content is truncated and the queue is unchanged.
- A promptable session dispatches immediately without cancellation. A busy
session uses the existing backend-owned cancellation progress, cancels only
the turn observed when the action began, and starts the replacement turn
after cancellation settles.
- If ordinary FIFO delivery has reserved a queued entry but has not yet
accepted its prompt, Send Now wins that same-session handoff. The reserved
source is restored before the requested selection is claimed, so an
all-scope replacement can include it in one aggregate prompt. Once FIFO has
accepted its prompt, Send Now fails closed with `send_now_conflict`; it does
not duplicate or cancel that successor turn.
- Send Now is a replacement/steering cancellation. It does not create the
ordinary **Turn cancelled** message, move the task to review, evaluate
`cancel_triggers_turn_complete`, or run the cancelled turn's
`on_turn_complete` actions.
- The action is disabled while its request or any backend cancellation for the
session is pending. The backend also rejects overlapping Send Now or explicit
cancellation operations so rapid clicks and multiple clients cannot create
successor turns.
- Auto-run becomes ON in the same repository transaction that claims the exact
Send Now selection. Validation, cancellation, conflict, or queue-change
failures before that claim leave the previous policy unchanged. If the
accepted asynchronous handoff later restores the claim, Auto-run remains ON
because Send Now was also an explicit resume request. The all-entry scope
follows the same rule.
- Successful dispatch publishes the existing queue-status and session
cancellation updates. The initiating client refetches the authoritative queue
after success or failure.
## API Surface
New WebSocket action:
```text
message.queue.send_now
```
Request:
```json
{
"session_id": "session-id",
"scope": "entry",
"entry_id": "queue-entry-id"
}
```
or:
```json
{
"session_id": "session-id",
"scope": "all"
}
```
`scope` is required and is exactly `entry` or `all`. `entry_id` is required
only for `entry`; malformed combinations are rejected with `validation`.
Successful response:
```json
{
"session_id": "session-id",
"dispatched": true,
"sent_count": 3
}
```
`sent_count` is the number of source queue entries represented by the new
turn. The action returns success only after the selection is claimed and handed
to the replacement-turn dispatch path.
Errors:
- `entry_not_found`: the selected entry is no longer visible and pending.
- `queue_empty`: the `all` snapshot contains no visible pending entries.
- `queue_changed`: one or more bulk-snapshot entries changed eligibility before
the atomic claim; no partial selection is dispatched.
- `send_now_conflict`: another cancellation or Send Now operation already owns
the session.
- `turn_changed`: the active turn changed while the operation waited; the
successor is left untouched.
- `send_now_attachment_overflow`: the aggregate exceeds an attachment limit.
- `send_now_reference_overflow`: the aggregate exceeds the entity-reference
limit.
- `session_not_promptable`: the session has neither an interruptible active
turn nor a promptable state.
All requests require access to `session_id`. Authorization happens before any
queue read, cancellation, or mutation and uses the existing non-enumerating
session-not-found response.
## State and Concurrency
The orchestrator snapshots the active turn and, for `scope=all`, the ordered
visible entry IDs before beginning cancellation. All validation and aggregation
limits are checked before the cancellation signal is sent.
The cancel-and-dispatch decision uses the shared per-session cancellation and
queue-take serialization point. An ordinary FIFO handoff has an explicit
pre-acceptance phase: Send Now may supersede that reservation while the worker
has not claimed prompt ownership, but the worker must claim ownership before
creating a visible user message, running turn-start workflow effects, or
accepting the agent prompt. After that claim, the handoff is terminal for Send
Now and the existing conflict response applies.
When Send Now supersedes a pre-acceptance FIFO reservation, the backend
restores that exact source (including clearing a durable lifecycle
reservation), then atomically claims exactly the selected ID or the complete
bulk snapshot. New messages accepted after the snapshot remain queued. A
missing or newly ineligible bulk member fails the whole claim; the backend
never sends a partial bulk result or substitutes the FIFO head for a missing
selected entry.
Ordinary entries are removed when claimed, matching normal queue delivery.
Durable lifecycle entries remain reserved until the combined prompt is
accepted, then all durable source rows are acknowledged. A retryable dispatch
failure restores every ordinary source at its original FIFO position and
releases every durable reservation before publishing queue status.
The claim transaction also persists `auto_run=true`. This write is part of the
all-or-nothing selection mutation, so a rejected claim cannot partially resume
the queue. Restoration after an accepted claim restores message rows but does
not undo that explicit resume instruction.
## Permissions
- Any user who can access the session may invoke either Send Now action on its
visible pending entries, regardless of entry provenance.
- The action never accepts caller-supplied `task_id`, `user_id`, sender identity,
content, attachments, or metadata. Those values come from the authorized
session and persisted queue rows.
- Hidden durable entries already reserved for another delivery are not eligible.
## Failure Modes
- **Cancellation fails:** no queue selection is claimed; the entries remain
pending, Auto-run retains its prior value, and the UI refetches and shows a
localized error.
- **Selection changes during cancellation:** the active turn may already be
cancelled, but no replacement is dispatched from a partial or different
selection. The UI refetches and asks the user to retry.
- **FIFO handoff race:** if normal FIFO delivery reserved the head but has not
accepted its prompt, Send Now restores/reclaims that reservation and
dispatches the requested exact selection. If FIFO already accepted the
prompt, Send Now returns `send_now_conflict`; it leaves that successor and
the remaining queue authoritative rather than creating a duplicate turn.
- **Prompt admission fails after claim:** the backend restores the original
entries and their FIFO positions before publishing status. Auto-run stays ON
because the operation was already accepted as a resume. Existing ordinary
queue delivery retains its current process-crash window after a destructive
claim; durable lifecycle rows retain their accepted-until-acknowledged
guarantee.
- **Successor turn appears:** the operation fails with `turn_changed`, leaves
the successor running, and does not claim queue entries.
- **Conflicting explicit Cancel:** the already accepted operation wins. Send Now
never dispatches after a workflow-completing cancellation.
## Responsive and Mobile Behavior
- The existing inline queue panel remains the desktop and phone composition;
its queue list remains the only internal scroll owner and the composer stays
visible.
- Desktop row actions keep their current hover/focus disclosure. Phone and
coarse-pointer layouts expose every row's **Send Now** without hover and give
it at least a 44 by 44 CSS-pixel hit target.
- The header **Auto-run** switch, **Clear all**, and collapse controls remain
reachable without horizontal page overflow. The header may wrap on a narrow
phone rather than shrinking labels below usable targets.
- Desktop and mobile share the same queue hook, cancellation state, and backend
action. Mobile Playwright coverage uses touch input and proves the same
replacement-turn result.
## Scenarios
- **GIVEN** an agent is busy and three messages are queued, **WHEN** the user
clicks row **Send Now** on the second message, **THEN** the active turn is
interrupted, Auto-run becomes ON, the second message starts the replacement
turn, and after that turn completes the first and third messages run as
separate turns in their original relative order without another
queue-control click.
- **GIVEN** Auto-run is OFF with queued A, B, and C, **WHEN** the user clicks
Send Now on B, **THEN** B runs first and the preserved A, C remainder
continues automatically as separate turns.
- **GIVEN** an authorized protocol client selects all three visible messages,
**WHEN** it sends `message.queue.send_now` with `scope: "all"`, **THEN** one
replacement turn receives the three message bodies concatenated in FIFO
order and the queue becomes empty.
- **GIVEN** queued messages contain attachments and repeated entity references,
**WHEN** a protocol client requests `scope: "all"`, **THEN** the replacement
prompt contains all attachments in FIFO order and one canonical copy of each
reference.
- **GIVEN** a bulk aggregate exceeds an existing message limit, **WHEN** a
protocol client requests `scope: "all"`, **THEN** the active turn continues,
the queue is unchanged, and the stable limit error is returned.
- **GIVEN** the session is already promptable with queued work, **WHEN** the
user invokes row Send Now or a protocol client requests `scope: "all"`,
**THEN** the requested selection starts without issuing a cancellation.
- **GIVEN** a promptable session has two queued messages and normal FIFO
delivery has reserved the first one but has not accepted its prompt, **WHEN**
a protocol client requests `scope: "all"`, **THEN** the FIFO reservation is
restored, both messages are claimed in FIFO order, and exactly one
replacement prompt contains both bodies.
- **GIVEN** normal FIFO delivery has already accepted its successor prompt,
**WHEN** the user clicks Send Now, **THEN** the action fails closed without a
duplicate user message, duplicate prompt, or cancellation of that successor,
and the authoritative remaining queue is preserved.
- **GIVEN** the workflow step enables `cancel_triggers_turn_complete`, **WHEN**
the user sends a queued message now, **THEN** the task stays on the same step
and only the replacement turn's ordinary lifecycle hooks may change it.
- **GIVEN** a successor turn replaces the observed active turn while Send Now
waits, **WHEN** the operation revalidates, **THEN** it leaves the successor
running and the requested queue entries pending.
- **GIVEN** a phone viewport with a busy agent and queued messages, **WHEN** the
user opens the queue panel, **THEN** the Auto-run switch and every row's Send
Now control are visible, touch-sized, and contained without horizontal
overflow.
## Out of Scope
- Reordering queued messages before sending; that capability is governed by
[Reorder Queued Messages](message-queue-reorder.md), and bulk Send Now
dispatches in the current (reordered) FIFO order.
- Choosing a different model or plan mode for the bulk turn.
- Sending an arbitrary user-selected subset other than one entry or all visible
pending entries.
- Changing **Remove**, **Clear all**, edit, or merge semantics. FIFO participates
in Send Now handoff arbitration only until its prompt is accepted. Normal
FIFO policy and first-party queue controls are governed by
[Control Pending Message Auto-run](message-queue-run.md).
- Making ordinary queued-message dispatch crash-durable after its existing
destructive claim boundary.