Files
kandev/docs/plans/message-queue-management/plan.md
T
2026-08-24 11:09:03 +00:00

189 lines
9.5 KiB
Markdown

---
spec: docs/specs/ui/requirements/message-queue-management.md
decision: docs/decisions/2026-08-03-separate-message-queue-provenance-cancellation-and-capacity.md
created: 2026-08-03
status: implemented
---
# Fix Plan: Message Queue Management
## Overview
Make the queue panel's **Clear all** promise literal, add removal for every
visible pending message, and expose the per-session queue limit under
**Settings > General > Message Queue**. Preserve durable in-flight delivery,
session isolation, message provenance, and already accepted retries.
## Confirmed Root Cause
The reported Bitbucket Integration task has ten persisted queue rows. Every row
has `queued_by='agent'`. The frontend sends `message.queue.cancel`, whose
handler calls `messagequeue.Service.CancelAll`; the SQLite and memory
repositories then delete only rows whose `queued_by` is not `agent`,
`workflow`, or `server`. The response therefore says `removed: 0`. The hook's
optimistic empty state is replaced by the next authoritative status event, so
the full queue immediately reappears.
Individual removal has the same repository guard. The React row action also
uses one `canEdit` flag for both **Edit** and **Remove**, hiding removal on
agent/workflow rows. This behavior implemented ADR 0051's prior ownership
boundary, so the repair requires an explicit contract change rather than only
a frontend patch.
The live installation was inspected read-only. No queued row, task, session,
or setting was mutated during diagnosis.
## Behavioral Contract
- An authorized session owner may remove any pending entry returned by queue
status and may clear all such entries, regardless of provenance.
- `queued_by` continues to control editing and merge compatibility.
- The visible queue uses compact one-based ordinals after remove, merge, or
drain; durable FIFO position keys remain unchanged.
- A durable lifecycle row already marked in flight remains hidden and
non-cancellable. Cancellation and reservation must be race-safe.
- Every user-facing queue WebSocket handler authorizes its session. Queue add
and append additionally validate that `task_id` belongs to `session_id`.
- Capacity resolves as valid environment value, then persisted install
setting, then default `10`. `0` means unlimited.
- Saving capacity applies live, does not prune existing rows, and limits only
new admissions. Restore/retry of already accepted work bypasses the cap.
## Backend Design
### Pending-row cancellation
- Change the `Repository.DeleteByID` and `DeleteAllBySession` contracts from
user-origin deletion to visible-pending deletion.
- In both SQLite/Postgres-compatible and memory repositories, serialize with
the existing per-session mutation lock and skip
`lifecycle_reserved_in_flight` rows.
- Use persisted compare guards and affected-row counts so an out-of-process
reserve or update that wins cannot be deleted or counted afterward.
- Preserve the mandatory `(session_id, entry_id)` scope and the privileged
`PurgeTask`/`AcknowledgeByID` paths.
- Update service logging and error text to remove the obsolete "not owned by
caller" implication.
### Queue action authorization
- Add a task-service authorization method for a `(task_id, session_id)` pair:
authorize both resources, load the session, and fail closed when the pair is
inconsistent.
- Inject a queue access authorizer into `QueueHandlers` from backend
composition.
- Guard get, add, append, update, remove, merge, clear, and drain before any
queue read or mutation. Add/append use pair authorization; session-only
actions use session authorization.
- Return the existing non-enumerating error shape and test that unauthorized
calls neither reveal status nor publish a mutation event.
### Runtime capacity setting
- Add `internal/system/queuesettings` with typed settings, generic-settings
adapter, environment resolver, live target interface, service, and HTTP
handlers.
- Persist `{"max_per_session": N}` under install-wide key `message_queue`.
- Resolve the setting before the orchestrator queue starts. Extend system
wiring with the live `messagequeue.Service` so GET/PATCH use the same target
and register under `/api/v1/system/message-queue/settings`.
- Register GET on the signed-in system group and PATCH on its admin subgroup.
PATCH validates `N >= 0`, returns `409` while a valid environment override is
active, persists first, then live-applies the response value.
- Replace the queue service's plain integer with a concurrency-safe runtime
value and add `SetMaxPerSession`. Each admission snapshots it once for the
repository call, log, and `queue_full` response.
- Normalize non-positive environment values to effective `0`; ignore and warn
on malformed values. Preserve `KANDEV_QUEUE_MAX_PER_SESSION` compatibility.
- Make ordinary restore and durable lifecycle retry bypass the current
admission cap. Initial user/agent/workflow/server insertions still obey it.
## Frontend Design
### Queue panel
- Split `canEdit` from `canRemove` in `QueuedGhostMessage` and `RowActions`.
Every visible row gets remove; editing remains user-only and merge rules stay
unchanged.
- Keep optimistic behavior only when the hook can reconcile it. On removal or
clear failure/race, refetch authoritative status and show localized feedback.
- Replace touched queue-header literals with `chat` namespace keys.
- Keep compact hover controls on pointer devices. On coarse-pointer viewports,
make remove, clear, close, expand, merge, and run-next actions at least 44px
high/wide and always visible.
### General settings page
- Add API types and fetch/update helpers for the configured/effective response.
- Add a client settings card with a numeric `min=0` input, explicit unlimited
help, source badge/help, loading/error states, environment lock state, and
member read-only state.
- Register the draft with `useSettingsSaveContributor`; do not add local
Save/Cancel controls.
- Add `/settings/general/message-queue` to the Vite route registry, app-page
composition, General sidebar, breadcrumb label map, and sidebar-navigation
tests. Redirect the former `/settings/system/message-queue` URL to preserve
existing bookmarks.
- Add all new copy to translation catalogs and regenerate/check pseudo locale.
Avoid module-scope `t()` by storing label keys and translating during render.
## Responsive and Mobile Contract
The nearest shipped queue exemplar is
`apps/web/e2e/tests/chat/mobile-message-queue-management.spec.ts`: retain its inline
panel and single queue scroll region so the composer stays visible. The new
controls must not depend on hover and must meet the 44px coarse-pointer target.
The settings page uses the existing left-side mobile Settings sheet and shared
`settings-scroll-container`. Render one column with no nested page scroller;
keep the numeric field at least 44px high and above the existing save bar plus
safe-area padding. Desktop and mobile expose identical values and permissions.
## TDD and Verification Strategy
1. Change existing backend tests to expect agent/workflow/server pending rows to
be removable and add in-flight/race/security regressions. Run them RED
against unchanged production code.
2. Implement cancellation and handler authorization; run memory, SQLite, and
env-gated Postgres package tests GREEN.
3. Add capacity resolver/store/service/handler tests, including live apply,
environment precedence, lower-than-current behavior, and retry restoration.
Run RED, then implement and rerun GREEN.
4. Add frontend component/hook/settings tests first, including split
edit/remove permissions and environment/member locks. Implement minimal UI
changes, then run typecheck, lint, and i18n gates.
5. Add desktop and mobile Playwright scenarios for mixed/agent provenance and
settings navigation/save. Preserve and restore the install-wide cap in test
teardown so parallel specs cannot inherit modified state.
6. Update public queue/configuration/operations docs and run their validators.
## Implementation Waves
- [x] [Task 01: Make pending queue cancellation authoritative](task-01-authoritative-queue-cancellation.md) — wave 1
- [x] [Task 02: Add live install-wide queue capacity settings](task-02-live-queue-capacity-settings.md) — wave 2, after Task 01
- [x] [Task 03: Expose removal for every visible queue row](task-03-visible-queue-removal-ui.md) — wave 3, after Task 01
- [x] [Task 04: Add the Message Queue General settings page](task-04-message-queue-settings-ui.md) — wave 3, after Task 02
- [x] [Task 05: Prove end-to-end behavior and update public docs](task-05-e2e-and-public-docs.md) — wave 4, after Tasks 03 and 04
Tasks 03 and 04 are file-disjoint enough to run in parallel after their backend
dependencies, but this package does not authorize subagents. Implementation
remains in the user-controlled primary session unless the user later delegates.
## Environment Prerequisite
If `apps/node_modules` is absent, run `pnpm install --frozen-lockfile` from
`apps/` before frontend RED tests. Do not change the lockfile.
## Risks and Boundaries
- A clear/remove request may lose to reservation; that message can still be
delivered. The UI must reconcile rather than claim it cancelled in-flight
work.
- Dynamic lowering must not make restore or lifecycle retry lossy.
- Settings tests mutate install-wide state and must restore their baseline even
after failure.
- Queue contents from the diagnosed live Bitbucket task remain untouched until
this package is implemented and deployed.
- Editing backend-origin content, changing merge rules, queue reordering,
per-workspace limits, and automatic pruning remain out of scope.