296 lines
13 KiB
Markdown
296 lines
13 KiB
Markdown
---
|
|
status: shipped
|
|
created: 2026-08-16
|
|
owner: kandev
|
|
---
|
|
|
|
# Control Pending Message Auto-run
|
|
|
|
Decision:
|
|
[ADR-2026-08-16-server-owned-queue-auto-run](../../decisions/2026-08-16-server-owned-queue-auto-run.md)
|
|
|
|
Implementation plan:
|
|
[Unify Queue Run Controls](../../plans/message-queue-run-controls/plan.md)
|
|
|
|
## Why
|
|
|
|
The queue header presents **Run next** and bulk **Send Now** as separate ways
|
|
to move pending work. **Run next** directly takes only one FIFO head, but normal
|
|
turn completion then keeps taking later entries. The label describes one
|
|
backend step rather than the user's real outcome: putting the queue in motion.
|
|
|
|
Users also cannot ask the queue to stop after the current response. Explicit
|
|
Cancel parks pending work, but it cuts off the response and may complete a
|
|
workflow step. The queue needs a persistent run policy that can be changed
|
|
without cancelling the active turn.
|
|
|
|
## What
|
|
|
|
### Auto-run control
|
|
|
|
- The expanded queue header exposes one labeled **Auto-run** switch. It
|
|
replaces header **Run next** and header bulk **Send Now**.
|
|
- The switch has visible state-specific help:
|
|
- ON: “Runs queued messages one at a time.”
|
|
- OFF: “Finishes the current response, then queued messages wait.”
|
|
- ON means the backend may start the FIFO head whenever the session is eligible
|
|
and may continue with one distinct turn per entry after each prior turn
|
|
completes.
|
|
- OFF prevents every later automatic FIFO take. It does not cancel, truncate,
|
|
or otherwise alter a turn that is already active or a queued handoff already
|
|
accepted before OFF won the race.
|
|
- Turning ON while the session is promptable attempts the current FIFO head
|
|
immediately. When the session is busy or another lifecycle guard applies,
|
|
ON is persisted and delivery resumes at the next eligible backend trigger.
|
|
- The policy is backend-owned per session, defaults to ON, survives an empty
|
|
queue, navigation, reload, and backend restart, and remains OFF until an
|
|
explicit resume operation changes it.
|
|
- The switch stays visible while queued work exists, including during
|
|
clarification. It may change the future policy but never bypasses
|
|
clarification, workflow-transition, cancellation, prompt-admission, or
|
|
session-lifecycle safeguards.
|
|
- The switch is disabled while its mutation or another conflicting queue or
|
|
cancellation operation is pending. Failure restores the authoritative state
|
|
and shows a localized error.
|
|
|
|
### Targeted Send Now
|
|
|
|
- Every visible queue row, including the FIFO head, retains **Send Now**. No
|
|
separate **Skip to next** action is introduced because Send Now already
|
|
selects the message to skip to.
|
|
- A successful per-row Send Now is both a targeted priority override and an
|
|
explicit resume. It turns Auto-run ON, dispatches the selected entry first,
|
|
and then lets the preserved FIFO remainder continue as separate turns.
|
|
- Selecting a later row is a transient promotion, not a persisted reorder. For
|
|
queued A, B, C, Send Now on B produces B, A, C while A and C keep their
|
|
relative order.
|
|
- The first-party header no longer aggregates all queued rows into one prompt.
|
|
Existing protocol clients may continue using `message.queue.send_now` with
|
|
`scope: "all"`; an accepted request also turns Auto-run ON.
|
|
- The replacement-turn and restoration guarantees in
|
|
[Send Queued Messages Now](message-queue-send-now.md) remain authoritative.
|
|
|
|
### Cancel and other queue controls
|
|
|
|
- Auto-run OFF is the non-destructive way to finish the current response and
|
|
hold the backlog. Explicit Cancel remains the immediate-stop action and keeps
|
|
its configured workflow-completion behavior.
|
|
- When explicit Cancel leaves pending entries, backend queue state becomes OFF
|
|
before cancellation completes. The displayed switch therefore matches the
|
|
parked result. Internal cancellation and Send Now's replacement cancellation
|
|
do not turn Auto-run OFF.
|
|
- Reorder, edit, merge, Remove, Clear all, pin, and collapse retain their
|
|
existing behavior. Clearing or draining the final row does not reset the
|
|
saved Auto-run policy.
|
|
|
|
## API Surface
|
|
|
|
New WebSocket action:
|
|
|
|
```text
|
|
message.queue.auto_run.set
|
|
```
|
|
|
|
Request:
|
|
|
|
```json
|
|
{
|
|
"session_id": "session-id",
|
|
"enabled": false
|
|
}
|
|
```
|
|
|
|
Successful response:
|
|
|
|
```json
|
|
{
|
|
"session_id": "session-id",
|
|
"auto_run": false,
|
|
"dispatched": false
|
|
}
|
|
```
|
|
|
|
`dispatched` is `true` only when this ON request also reserved and launched a
|
|
promptable FIFO head. A valid ON request still succeeds with `false` when the
|
|
queue is empty, the current turn is busy, clarification is pending, or another
|
|
normal lifecycle guard defers delivery. OFF always returns `false`.
|
|
|
|
The existing `message.queue.get` response and
|
|
`message.queue.status_changed` payload gain:
|
|
|
|
```json
|
|
{
|
|
"auto_run": true
|
|
}
|
|
```
|
|
|
|
The field is always present. Missing persisted state is projected as `true`.
|
|
|
|
Existing actions remain compatible:
|
|
|
|
```text
|
|
message.queue.drain
|
|
request: { session_id: string }
|
|
response: { session_id: string, drained: boolean }
|
|
|
|
message.queue.send_now
|
|
request: { session_id: string, scope: "entry", entry_id: string }
|
|
response: { session_id: string, dispatched: true, sent_count: 1 }
|
|
```
|
|
|
|
A successful drain enables Auto-run before returning. An accepted Send Now
|
|
claim enables Auto-run in the same queue transaction that claims its exact
|
|
selection. Pre-claim validation, conflict, and queue-change errors leave the
|
|
previous policy unchanged.
|
|
|
|
All mutations require access to `session_id`. Authorization happens before
|
|
queue reads or state changes and uses the existing non-enumerating
|
|
session-not-found response.
|
|
|
|
## Data Model
|
|
|
|
Queue-owned state uses a dedicated table in both supported SQL dialects:
|
|
|
|
```sql
|
|
CREATE TABLE queue_session_state (
|
|
session_id TEXT PRIMARY KEY,
|
|
auto_run INTEGER NOT NULL DEFAULT 1
|
|
);
|
|
```
|
|
|
|
The Postgres-compatible definition uses the repository's normal boolean
|
|
binding conventions. The memory repository stores the same value under its
|
|
existing mutex. Absence means ON, so upgrades require no backfill and existing
|
|
sessions keep today's behavior.
|
|
|
|
Policy mutations and policy-aware head reservations take the existing
|
|
per-session admission and `queue_session_locks` synchronization. The state row
|
|
is separate from that lock row because one is product policy and the other is
|
|
cross-process coordination.
|
|
|
|
Workflow session transfer moves queued entries, pending move, and Auto-run
|
|
policy in the same transaction. The destination is ON only when both source
|
|
and destination were ON; otherwise pause wins. The old session's policy row is
|
|
removed. Ordinary queue snapshot/restore changes entries and pending moves but
|
|
does not roll back an independent policy choice.
|
|
|
|
## State and Concurrency
|
|
|
|
```text
|
|
toggle OFF
|
|
auto-run ON ----------------------------------> auto-run OFF
|
|
| |
|
|
| eligible + queued | current turn may finish
|
|
v | later entries wait
|
|
distinct FIFO turn --ready--> next distinct turn |
|
|
^ |
|
|
+---------- toggle ON or accepted Send Now <-----+
|
|
```
|
|
|
|
The backend linearizes an automatic reservation against OFF in one queue
|
|
transaction:
|
|
|
|
- If OFF commits first, the automatic reservation observes OFF and leaves the
|
|
FIFO head pending.
|
|
- If the reservation commits first, that message may start. The acknowledged
|
|
OFF state still prevents the following entry.
|
|
|
|
An accepted Send Now claim changes Auto-run to ON atomically with selection.
|
|
If its asynchronous prompt handoff later fails and restores the claimed rows,
|
|
Auto-run remains ON because the accepted user operation explicitly resumed the
|
|
queue. A failure before claim leaves the old policy untouched.
|
|
|
|
Auto-run governs all automatic FIFO takes, including user, agent, workflow,
|
|
server, lifecycle, and CI-automation entries. A paused automation entry remains
|
|
safely queued; the producer must not translate policy deferral into a delivery
|
|
failure. Exact targeted dispatch paths remain explicit: Send Now resumes by
|
|
contract, while unrelated direct prompts that do not consume the pending queue
|
|
are outside this policy.
|
|
|
|
## Permissions
|
|
|
|
Any user who can access the session may change its Auto-run policy or invoke a
|
|
row's Send Now action. The browser supplies only session ID, desired boolean,
|
|
and, for Send Now, persisted entry identity. It never supplies task ownership,
|
|
message content, provenance, or sender identity.
|
|
|
|
## Failure Modes
|
|
|
|
- **Policy persistence fails:** no success is reported, no optimistic state is
|
|
retained, and the client refetches queue status.
|
|
- **ON cannot dispatch immediately:** the request still persists ON and returns
|
|
`dispatched: false`; the next eligible backend trigger retries normally.
|
|
- **OFF races a head reservation:** the transaction ordering described above
|
|
decides whether that head is already current. OFF always governs later rows.
|
|
- **Send Now fails before claim:** the prior Auto-run value and queue remain
|
|
authoritative.
|
|
- **Send Now fails after accepted claim:** its existing restoration path
|
|
restores the exact entries; Auto-run stays ON.
|
|
- **Explicit Cancel leaves a backlog:** cancellation parks it and publishes OFF
|
|
in the same authoritative status stream.
|
|
- **A clarification or workflow guard is active:** ON remains visible but does
|
|
not force a prompt through the guard.
|
|
- **A session transfer combines policies:** OFF wins so moved work never starts
|
|
due only to an identity change.
|
|
|
|
## Responsive and Mobile Behavior
|
|
|
|
- Desktop and phone keep the existing inline queue panel and its single
|
|
internal scroll owner. No drawer or mobile-only control path is introduced.
|
|
- The visible **Auto-run** label, state, and helper may wrap to a second header
|
|
line on narrow screens. Clear all, collapse, and the desktop-only pin keep
|
|
their existing hierarchy, and the panel creates no document-level horizontal
|
|
overflow.
|
|
- The switch and its label form one effective coarse-pointer target at least 44
|
|
by 44 CSS pixels. It is reachable without hover and uses the shared Switch
|
|
primitive's keyboard, focus, checked, disabled, and screen-reader semantics.
|
|
- Every row's Send Now remains always discoverable on coarse pointers with its
|
|
existing touch-sized target. Desktop may retain hover/focus disclosure.
|
|
- Desktop and mobile share queue state, mutation logic, errors, and WebSocket
|
|
reconciliation. Responsive code changes only sizing and wrapping.
|
|
|
|
## Scenarios
|
|
|
|
- **GIVEN** Auto-run is ON with queued A, B, and C, **WHEN** the current turn
|
|
completes, **THEN** A, B, and C run as three separate FIFO turns without
|
|
another queue-control click.
|
|
- **GIVEN** a response is active with A, B, and C queued, **WHEN** the user
|
|
turns Auto-run OFF before the next reservation wins, **THEN** the active
|
|
response finishes and A, B, and C remain pending.
|
|
- **GIVEN** Auto-run is OFF and the session is promptable, **WHEN** the user
|
|
turns it ON, **THEN** A starts immediately and later entries continue one at
|
|
a time.
|
|
- **GIVEN** Auto-run is OFF with A, B, and C queued behind an active response,
|
|
**WHEN** the user selects Send Now on B, **THEN** Auto-run turns ON, B replaces
|
|
the active turn without ordinary Cancel side effects, then A and C run as
|
|
distinct turns in that relative order.
|
|
- **GIVEN** Auto-run is OFF, **WHEN** the queue becomes empty and the browser
|
|
reloads or Kandev restarts, **THEN** the next queued entry remains held and
|
|
the panel reports OFF until the user resumes it.
|
|
- **GIVEN** explicit Cancel leaves pending entries, **WHEN** cancellation
|
|
settles, **THEN** the entries remain pending and Auto-run reports OFF.
|
|
- **GIVEN** clarification is pending with queued work, **WHEN** the user turns
|
|
Auto-run ON, **THEN** the switch reports ON but no queued prompt starts until
|
|
clarification is resolved and the session becomes eligible.
|
|
- **GIVEN** paused queued work transfers to another workflow session, **WHEN**
|
|
the transfer commits, **THEN** the destination remains OFF and no transferred
|
|
entry starts solely because its session ID changed.
|
|
- **GIVEN** a phone viewport with queued work, **WHEN** the user taps Auto-run
|
|
OFF and reloads, **THEN** the switch remains OFF, the backlog is held, all
|
|
controls are touch-sized, and the page has no horizontal overflow.
|
|
- **GIVEN** an existing protocol client sends Send Now with `scope: "all"`,
|
|
**WHEN** the backend accepts the request, **THEN** its aggregate replacement
|
|
behavior remains compatible and Auto-run becomes ON for later arrivals.
|
|
|
|
## Out of Scope
|
|
|
|
- Scheduling a future run time, rate limiting, or pausing after an arbitrary
|
|
number of additional entries.
|
|
- A global, workspace, or task-wide Auto-run preference. Policy is per session.
|
|
- Stopping or rolling back a turn or queue handoff already accepted before OFF
|
|
won the reservation race.
|
|
- Combining queued rows into one prompt through the first-party queue UI.
|
|
- Removing the backward-compatible Send Now all-entry or drain protocols.
|
|
- Changing queue capacity, merge, reorder, edit, Remove, Clear all, pin, or
|
|
collapse semantics.
|