12 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/ui/message-queue-auto-merge.md | 2026-08-12 | completed |
Implementation Plan: Automatically Merge Consecutive Queued Messages
Overview
Add a default-on, install-wide automatic-merge setting without changing the manual merge switch. Build the strict tail-compatibility mutation first, route ordinary admission and staged attachment admission through it safely, then add settings persistence and UI. Finish with desktop/mobile E2E coverage and public documentation.
Backend
Automatic tail-merge contract
- Add a repository operation in
apps/backend/internal/orchestrator/messagequeue/repository.gothat targets one newly inserted source ID and its immediate predecessor. It returns the surviving target plus whether a fold occurred; incompatibility is a normalfalseresult, notErrNoMergeTarget. - Implement it under the existing per-session lock in
repository_memory.goand as one transaction inrepository_sqlite.go(the database-backed implementation also serves the env-gated Postgres suite). Select by source ID and session, select only the greatest lower position, compute the complete candidate, update the target, and delete the exact source. A missing/drained source never causes a search for another source. - Keep manual
MergeIntoAboveandmergeAllowedunchanged. Add automatic-only helpers inmerge.go:- source compatibility: same task/model/plan mode; user identities equal and
non-reserved, or agent identities equal with one identical non-empty
sender_task_id; lifecycle/workflow/server/clarification rows excluded; - non-additive metadata comparison after normalizing JSON-round-tripped map values;
- stable entity-reference and context-file unions;
- combined attachment count/byte validation using the same limits and byte accounting as send-now;
- content joining that adds
\n\nonly when both bodies are non-empty.
- source compatibility: same task/model/plan mode; user identities equal and
non-reserved, or agent identities equal with one identical non-empty
- Preserve the target's immutable fields and target-then-source order. Any mismatch or limit overflow leaves both rows unchanged and returns a skipped result.
Admission integration
- Extend
messagequeue.Serviceinservice.gowith a separate atomicautoMergeEnabledvalue, defaulting to true, plus getters/setters. Do not reusemergeEnabled; test all four switch combinations. - Within
QueueMessageWithMetadata, snapshot capacity and automatic-merge state underWithSessionAdmission, perform the existing capped insert, then attempt the repository fold. Capacity remains an admission prerequisite. Return the surviving target on success and the source on skip. - Treat a post-insert automatic-fold storage error as degraded success: the transaction leaves the accepted source separate, the service logs the failure, and callers do not receive an error that encourages a duplicate retry.
- Add a narrow deferred-finalization path for user WebSocket admissions that
contain staged file-backed attachments. Hold
WithSessionAdmissionacross insertion, claim, fold, and rollback so no later admission can merge into the provisional source.queue_handlers.gomust insert a distinct source first, claim its attachments, and only then finalize the snapshotted automatic-merge decision. On claim failure, the existing rollback removes only that source ID. Inline attachments can use ordinary admission. - Preserve exact-ID consumers.
QueueAndInterruptForPeerMessageandmessage_task_kandevuse the returned surviving ID, so an immediate parent interrupt takes the complete merged entry. Workflow/coalesce, restoration, explicit append, clarification, and reserved lifecycle paths remain excluded by their APIs or compatibility checks. - Publish
message.queue.status_changedonly after the final merge-or-separate state is known. Do not addauto_merge_enabledtoQueueStatus; the queue panel has no setting control.
Persistent settings and startup wiring
- Extend
apps/backend/internal/system/queuesettings/types.gowithAutoMergeEnabledonSettings,SettingsPatch, andEffective. Apply and validate it like the independent manual merge boolean. - Extend
store.go's pointer-backed legacy decoder so an omittedauto_merge_enabledbecomes true while an explicit false survives. Saving writes the normalized three-field object. - Extend
resolver.goandservice.goso GET/PATCH round-trip the field, partial updates preserve both other values, max-capacity environment locks do not block an auto-only patch, persistence precedes live apply, and the queue target receives all three effective values. In particular, an auto-only or manual-only PATCH under an environment capacity override must reapply the resolved environment capacity, not the lower persisted capacity. - In
apps/backend/internal/backendapp/orchestrator.go, resolve and apply the saved automatic setting before the orchestrator accepts work. Updatemessage_queue_settings_test.go,system_routes_test.go, andinternal/system/queuesettings/queuesettings_test.gofor default, upgrade, explicit false, partial PATCH, environment lock, restart, and live-apply cases.
Frontend
Settings API and types
- Add
auto_merge_enabledtoMessageQueueSettingsValueinapps/web/lib/types/system.ts. The existing partial patch type then accepts an auto-only update without changing the HTTP client contract. - Update
apps/web/lib/api/domains/settings-api.test.tsfixtures and assertions to prove the field is decoded and sent alone.
Message Queue settings card
- Extend
useMessageQueueSettingsLoadanduseMessageQueueSettingsDraftinapps/web/components/settings/system/message-queue-settings.tsxwith a third draft/baseline. Include it in contributor revision, dirty/valid/save/discard logic, and concurrency-safe response reconciliation. - Add a separate
AutoMergeToggleFieldspresentation block below the existing manual merge block. Use the label Automatically merge consecutive messages, explanatory same-source/fallback copy, a stable test ID, and the existing admin/member and shared floating-save behavior. - Keep the current Task Behavior page, card width, single settings scroll owner, and one-column responsive layout. The switch and surrounding row must remain touch-safe and clear the mobile save bar. No queue-panel component changes.
- Add all user-facing copy to
apps/web/src/locales/en/system.json, regenerate the pseudo catalog, and run i18n checks and the changed-line ratchet.
Tests
- What: strict same-source compatibility, identity preservation, ordered
content/context/attachment merge, metadata mismatch fallback, combined-limit
fallback, and no half-write.
File: new
apps/backend/internal/orchestrator/messagequeue/repository_auto_merge_test.goplus env-gated Postgres cases. How: table-drive the same contract over memory, SQLite, and Postgres; compare complete pre/post snapshots on skipped and failing paths. - What: default-on service admission, disabled behavior, three-message
chaining, cap-before-merge, concurrent admissions/drain, degraded repository
error, and independence from manual merge.
File: new
service_auto_merge_test.goand focused updates to existing queue tests that intentionally need multiple compatible rows. How: RED service tests first, then race tests using the existing admission lock and repository hooks. - What: file-backed attachment claim succeeds before fold and claim failure
removes only the new source.
File:
queue_handlers_test.goor a newqueue_handlers_auto_merge_test.go. How: handler-to-real-memory-service tests with blocking/failing attachment claim fakes and byte-for-byte target assertions. - What: returned surviving IDs remain valid for normal peer queueing and
parent interrupt dispatch, while different senders, clarification, workflow,
lifecycle, plugin-source mismatch, append, coalesce, and restore do not
conflate.
File: focused cases in
internal/orchestrator,internal/mcp/handlers, andinternal/backendapptests. How: integration tests through existing real service fixtures. - What: settings default/upgrade/persistence/partial PATCH/live apply and
environment-lock independence, including retention of the environment's live
capacity after an auto-only PATCH.
File:
internal/system/queuesettings/queuesettings_test.go,internal/backendapp/message_queue_settings_test.go, andinternal/system/system_routes_test.go. How: store, service, HTTP, and startup wiring tests. - What: third draft loads, saves alone, discards, survives a concurrent
response, remains editable under only a max environment lock, and is disabled
for members.
File:
message-queue-settings.test.tsxandsettings-api.test.ts. How: component tests with the existing save-provider harness and API mocks.
E2E Tests
- Scenario: default-on setting persists and can be switched off/on without
changing capacity or manual merge.
File:
apps/web/e2e/tests/system/message-queue-settings.spec.ts. What to verify: GET default, auto-only PATCH body, shared save bar, reload, environment-cap lock independence, member read-only state, and full baseline restoration inafterEach. - Scenario: two compatible user messages become one when enabled and remain
two when disabled.
File:
apps/web/e2e/tests/chat/message-queue.spec.ts. What to verify: busy agent, final queue count/content/order, no transient second row after the response, and setting restoration even on failure. - Scenario: mobile exposes the same touch-safe switch without overflow,
nested scroll ownership, or save-bar overlap.
File:
apps/web/e2e/tests/system/mobile-message-queue-settings.spec.ts. What to verify: visible switch, minimum touch size, save/discard, and persisted value. - Existing management, manual merge, send-now, and reorder E2E scenarios that
require multiple compatible rows explicitly disable auto merge for their
setup and restore the prior install value during teardown. Update desktop and
mobile-*specs together.
Verification Results
- Backend repository, admission, handler, MCP, settings, startup, and race suites passed, including memory, SQLite, and environment-gated Postgres contracts.
- Frontend passed 34 focused component/API tests, typecheck, focused ESLint,
pseudo-locale generation,
i18n:check, andi18n:ratchet. - Desktop Chromium E2E passed 20 tests; mobile Chromium passed 4 tests; the focused attribution, steering, and sidebar-count audit passed 3 tests.
- Public documentation tests passed 60 tests, the validator accepted 41
published pages, and
git diff --checkpassed. - No blockers remain.
Review remediation verification also passed the focused automatic-merge repository/service tests and the message-queue race suite after adding the context-file cap and shared admission serialization for queue mutations.
Implementation Waves And Parallel Candidates
Wave 1:
- [x] [task-01-auto-merge-repository](task-01-auto-merge-repository.md)
Wave 2:
- [x] [task-02-auto-merge-admission](task-02-auto-merge-admission.md)
Wave 3:
- [x] [task-03-auto-merge-settings](task-03-auto-merge-settings.md)
Wave 4:
- [x] [task-04-auto-merge-settings-ui](task-04-auto-merge-settings-ui.md)
Wave 5:
- [x] [task-05-auto-merge-e2e](task-05-auto-merge-e2e.md)
Wave 6:
- [x] [task-06-auto-merge-docs](task-06-auto-merge-docs.md)
The dependency chain is sequential in the primary conversation. No wave authorizes subagents; only a later explicit user request can do that.
Environment prerequisite
If apps/node_modules is absent, run pnpm install --frozen-lockfile from
apps/ before frontend or E2E checks. Do not change the lockfile.
Risks and boundaries
- Automatic admission must not call the manual setting gate or loosen manual merge provenance rules.
- File attachment claim rollback is destructive to the just-inserted source ID; automatic fold must never run before that claim succeeds, and a later admission must never use the provisional source as its target.
- A post-insert fold error cannot be returned as an admission failure because a caller retry would duplicate accepted content.
- Existing tests frequently queue multiple compatible user rows. They must opt out explicitly when row count is part of the scenario, while product defaults stay on.
- No schema migration, feature flag, environment variable, retroactive sweep, queue-panel toggle, or capacity bypass is planned.