263 lines
12 KiB
Markdown
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)
|