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

265 lines
12 KiB
Markdown

---
spec: docs/specs/ui/message-queue-auto-merge.md
created: 2026-08-12
status: 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.go` that targets
one newly inserted source ID and its immediate predecessor. It returns the
surviving target plus whether a fold occurred; incompatibility is a normal
`false` result, not `ErrNoMergeTarget`.
- Implement it under the existing per-session lock in
`repository_memory.go` and as one transaction in `repository_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 `MergeIntoAbove` and `mergeAllowed` unchanged. Add automatic-only
helpers in `merge.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\n` only when both bodies are 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.Service` in `service.go` with a separate atomic
`autoMergeEnabled` value, defaulting to true, plus getters/setters. Do not
reuse `mergeEnabled`; test all four switch combinations.
- Within `QueueMessageWithMetadata`, snapshot capacity and automatic-merge
state under `WithSessionAdmission`, 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 `WithSessionAdmission` across
insertion, claim, fold, and rollback so no later admission can merge into the
provisional source. `queue_handlers.go` must 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. `QueueAndInterruptForPeerMessage` and
`message_task_kandev` use 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_changed` only after the final merge-or-separate
state is known. Do not add `auto_merge_enabled` to `QueueStatus`; the queue
panel has no setting control.
### Persistent settings and startup wiring
- Extend `apps/backend/internal/system/queuesettings/types.go` with
`AutoMergeEnabled` on `Settings`, `SettingsPatch`, and `Effective`. Apply and
validate it like the independent manual merge boolean.
- Extend `store.go`'s pointer-backed legacy decoder so an omitted
`auto_merge_enabled` becomes true while an explicit false survives. Saving
writes the normalized three-field object.
- Extend `resolver.go` and `service.go` so 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. Update
`message_queue_settings_test.go`, `system_routes_test.go`, and
`internal/system/queuesettings/queuesettings_test.go` for default, upgrade,
explicit false, partial PATCH, environment lock, restart, and live-apply
cases.
---
## Frontend
### Settings API and types
- Add `auto_merge_enabled` to `MessageQueueSettingsValue` in
`apps/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.ts` fixtures and assertions
to prove the field is decoded and sent alone.
### Message Queue settings card
- Extend `useMessageQueueSettingsLoad` and
`useMessageQueueSettingsDraft` in
`apps/web/components/settings/system/message-queue-settings.tsx` with a third
draft/baseline. Include it in contributor revision, dirty/valid/save/discard
logic, and concurrency-safe response reconciliation.
- Add a separate `AutoMergeToggleFields` presentation 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.go`
plus 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.go` and 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.go` or a new
`queue_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`,
and `internal/backendapp` tests.
**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`, and
`internal/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.tsx` and `settings-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 in `afterEach`.
- **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`, and `i18n: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 --check` passed.
- 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
```text
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.