Files
kandev/docs/specs/ui/message-queue-auto-merge.md
T
2026-08-13 08:43:26 +01:00

263 lines
12 KiB
Markdown

---
status: shipped
created: 2026-08-12
owner: kandev
---
# Automatically Merge Consecutive Queued Messages
## Why
Users and coordinating agents often send several short follow-ups while a
session is busy. Delivering every follow-up as a separate turn adds latency and
queue noise even when the messages came from the same source and can safely be
handled together.
## What
- **Settings > Task Behavior > Message Queue** exposes an
**Automatically merge consecutive messages** switch. The install-wide switch
is on by default.
- The switch applies immediately to messages admitted after the setting is
read. It does not compact entries that were already pending.
- When enabled, a newly admitted message is automatically folded into the
pending tail entry only when the two entries are consecutive and compatible.
- The earlier tail entry survives. It keeps its ID, FIFO position, queued time,
task, model, plan-mode value, and sender identity. Content and additive
context from the new entry follow the earlier entry in submission order.
- User entries have the same source only when both have the same non-reserved
`queued_by` identity. Agent entries have the same source only when both are
agent-owned and carry the same non-empty `sender_task_id`.
- Workflow, server, clarification, durable lifecycle, and reserved in-flight
entries are never automatic merge sources or targets. An agent entry without
`sender_task_id` is also excluded.
- Entries must have the same `task_id`, `model`, and `plan_mode`. Metadata other
than `entity_references` and `context_files` must be equivalent. This keeps
plugin source, sender-session provenance, parent-question state, workflow
state, and future metadata from being silently discarded.
- Merged content uses one blank line between two non-empty bodies. When either
body is empty, the other body is retained without an added leading or trailing
separator.
- Attachments retain target-then-source order. Automatic merging occurs only
when the combined attachment count and payload size remain within the shared
per-message limits.
- Entity references form a stable target-then-source union, deduplicated by
canonical reference, and must remain within the per-message reference limit.
Context files form a stable target-then-source union with exact duplicates
removed and remain capped at 200 descriptors. Exceeding that cap keeps the
newly admitted entry separate.
- If any compatibility, provenance, metadata, attachment, or reference check
fails, the newly admitted message remains a separate FIFO entry. Automatic
merge incompatibility never rejects an otherwise valid message.
- The existing manual **Merge with above** setting and action remain separate.
`merge_enabled` controls only the manual action; `auto_merge_enabled` controls
only admission-time automatic merging. Either switch may be enabled without
the other.
- No automatic-merge control is added to the queue panel. The queue panel
reflects the resulting authoritative queue after admission.
## Data model
The existing install-wide `settings` row with key `message_queue` gains one
boolean. Its normalized JSON value is:
```json
{
"max_per_session": 10,
"merge_enabled": true,
"auto_merge_enabled": true
}
```
- `auto_merge_enabled` defaults to `true` for a fresh installation.
- A persisted record that predates this field and omits it also resolves to
`true`.
- An explicit `false` remains false across reloads and backend restarts.
- The setting has no environment-variable override. The existing environment
lock for `max_per_session` does not lock either merge switch.
No queued-message schema changes. A successful automatic merge updates the
surviving target's content, attachments, and additive metadata, then removes
the new source entry. A fallback leaves both rows intact.
## API surface
The existing HTTP settings routes retain their authorization and partial-PATCH
contract:
```text
GET /api/v1/system/message-queue/settings
PATCH /api/v1/system/message-queue/settings
```
GET and PATCH responses include the new field in both views:
```json
{
"settings": {
"max_per_session": 10,
"merge_enabled": true,
"auto_merge_enabled": true
},
"effective": {
"max_per_session": 10,
"merge_enabled": true,
"auto_merge_enabled": true,
"source": "default",
"locked": false
}
}
```
PATCH may name only the new field:
```json
{ "auto_merge_enabled": false }
```
Omitted fields remain unchanged. A capacity environment lock blocks only a
PATCH that names `max_per_session`; an auto-only PATCH leaves the environment's
effective live capacity unchanged.
No new WebSocket action is added. Existing admissions, including
`message.queue.add` and inter-task queued delivery, return the surviving target
entry when an automatic merge succeeds and return the new source entry when it
stays separate. `message.queue.status_changed` publishes the final queue state.
The explicit `message.queue.append`, coalescing, restoration, retry, edit,
reorder, and manual merge contracts do not gain automatic behavior.
## Admission lifecycle and concurrency
1. An admission snapshots `auto_merge_enabled` and the current queue capacity.
2. Normal validation and capacity admission run first. A queue that is already
full still returns `queue_full`; automatic merge does not bypass the cap.
3. The new entry is durably inserted. File-backed attachments are claimed using
the existing task/session ownership checks before any fold is attempted.
4. When the snapshotted setting is on, the backend atomically checks the new
entry and its immediate predecessor and either folds the new entry into that
predecessor or leaves both entries unchanged.
5. The final authoritative queue is published.
Concurrent admissions for one session preserve their admission order. The
per-session admission serialization spans insertion, any staged attachment
claim, and the final fold or rollback. A later admission therefore cannot merge
into a provisional source whose attachment claim may still fail. The tail
check, target rewrite, and source removal are one repository mutation, so a
drain, remove, reorder, or manual merge can win before or after it but cannot
observe a half-merged row. If a concurrent action removes either candidate
before the automatic fold, no unrelated row is selected as a replacement
target.
## Permissions
- Signed-in install members may read the configured and effective values under
the existing system-settings rules.
- Only install admins may change `auto_merge_enabled`.
- Automatic merging grants no new queue mutation permission. It runs only as
part of an already-authorized admission and uses persisted sender provenance;
callers cannot name a target or claim a reserved sender identity.
## Failure modes
- **Incompatible tail:** the source remains a separate FIFO entry. No error is
shown solely because automatic merge was skipped.
- **Combined limit exceeded:** both messages remain separate; no attachment,
context file, or entity reference is truncated.
- **Attachment claim fails:** the newly inserted source entry is rolled back by
its own ID before automatic merge runs. An older target is never removed or
rewritten by this rollback.
- **Automatic fold storage failure after admission:** the transaction leaves
both rows separate, the failure is logged, and the already accepted message
is not reported as failed in a way that invites a duplicate retry.
- **Concurrent drain or mutation wins:** the fold is skipped or loses with no
partial write; the final queue status reconciles clients.
- **Invalid legacy settings JSON:** the existing invalid-setting fallback
applies. When the record is valid but omits `auto_merge_enabled`, the value is
`true`.
## Persistence guarantees
The saved switch survives backend restarts in the existing settings store. A
restart applies it before new queue admissions. Already merged rows remain
merged; disabling the switch does not reconstruct source rows. Already separate
rows remain separate; enabling the switch does not sweep them.
## Responsive and mobile behavior
- Desktop and mobile use the same Message Queue settings card, values, save
contributor, permissions, and validation.
- The new switch is a one-column settings row with a touch-safe control and
localized label and description.
- The existing `settings-scroll-container` remains the only page scroll owner.
The switch stays clear of the shared floating save bar and safe-area inset.
- The mobile queue panel does not gain a new control or scroll region.
## Scenarios
- **GIVEN** a fresh install, **WHEN** an admin opens Message Queue settings,
**THEN** automatic merge is shown as enabled.
- **GIVEN** a legacy `message_queue` setting without `auto_merge_enabled`,
**WHEN** the backend starts, **THEN** automatic merge is enabled and the next
save writes the normalized field.
- **GIVEN** automatic merge is enabled and two consecutive compatible user
messages are admitted, **WHEN** the second admission completes, **THEN** one
queued entry remains with both contents in order and the first entry's
identity.
- **GIVEN** automatic merge is disabled, **WHEN** the same two user messages are
admitted, **THEN** two FIFO entries remain.
- **GIVEN** two consecutive agent messages with the same non-empty
`sender_task_id`, **WHEN** the second is admitted, **THEN** they form one
agent-owned entry and the surviving ID can be targeted by immediate parent
interrupt delivery.
- **GIVEN** two agent messages from different sender tasks or sender sessions,
**WHEN** the later message is admitted, **THEN** it remains separate.
- **GIVEN** a user message follows an agent message, **WHEN** it is admitted,
**THEN** the two remain separate.
- **GIVEN** a workflow, server, clarification, or reserved lifecycle entry is a
source or tail candidate, **WHEN** another message is admitted, **THEN** no
automatic fold uses that entry.
- **GIVEN** two same-source entries differ in task, model, plan mode, plugin
source, parent-question metadata, or other non-additive metadata, **WHEN** the
later entry is admitted, **THEN** both remain separate with their metadata
unchanged.
- **GIVEN** compatible entries carry attachments, context files, and entity
references within all limits, **WHEN** they merge, **THEN** every value is
present in target-then-source order with only exact/canonical duplicates
removed.
- **GIVEN** a combined attachment, context-file, or entity-reference limit would
be exceeded, **WHEN** the later message is admitted, **THEN** both entries
remain separate and the admission succeeds if queue capacity permits.
- **GIVEN** a compatible file-backed message whose attachment claim fails,
**WHEN** admission rolls back, **THEN** the prior tail is byte-for-byte
unchanged.
- **GIVEN** the queue is already at its capacity, **WHEN** a compatible message
is submitted, **THEN** the existing `queue_full` response is returned and no
row changes.
- **GIVEN** automatic merge is on and manual merge is off, **WHEN** compatible
messages are admitted, **THEN** they merge automatically while the manual
action remains unavailable; the inverse switch combination leaves admissions
separate while preserving the manual action.
- **GIVEN** an environment value controls queue capacity, **WHEN** an admin
saves only `auto_merge_enabled`, **THEN** the automatic setting changes and
the environment-controlled effective and live capacity remain unchanged.
- **GIVEN** pending rows predate an off-to-on settings change, **WHEN** the
setting is saved, **THEN** those rows remain unchanged and only later
admissions are eligible.
- **GIVEN** a phone viewport, **WHEN** an admin changes the switch and saves,
**THEN** the value persists without horizontal overflow, nested scrolling, or
overlap with the shared save bar.
## Out of scope
- Retroactively compacting an existing queue.
- Merging non-consecutive entries or searching past an incompatible tail.
- A per-workspace, per-task, per-session, or queue-panel override.
- Changing the manual **Merge with above** compatibility or error behavior.
- Changing `message.queue.append`, workflow coalescing, delivery restoration,
retry, send-now, edit, or reorder semantics.
- Bypassing queue capacity because a message might merge.
## Implementation plan
[Implementation plan](../../plans/message-queue-auto-merge/plan.md)