Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
9.3 KiB
status, system, created, owners, requirements
| status | system | created | owners | requirements | ||
|---|---|---|---|---|---|---|
| draft | ui | 2026-09-02 |
|
|
Queued Message Editing System Design
Context and boundaries
The queue service owns pending-entry identity, FIFO admission, reservation, and mutation. The UI owns editor presentation and WebSocket request coordination. This design covers the cross-surface contract for editing a user-owned pending entry. It does not change agent, workflow, or server provenance rules, and it does not define the separate Send Now operation.
The queue's existing per-session admission boundary is the serialization point for queue mutations and automatic delivery. The edit hold is target-scoped: it must not become a session-wide Auto-run pause.
Components and responsibilities
internal/orchestrator/messagequeue.Serviceowns edit leases, target revisions, operation fencing, lease expiry, and the interaction between an edit hold and automatic head reservation.internal/orchestrator/handlers.QueueHandlersauthorizes the session before reading or mutating queue state, binds edit requests to the server-assigned WebSocket connection ID, translates typed queue conflicts to stable errors, and requests a post-save auto-run drain only after the edit lease is ended.internal/orchestrator.Serviceowns promptability checks and exposes the policy-preserving queue drain used after a successful edit save. This drain may reserve the FIFO head only when Auto-run is already enabled; it never enables Auto-run as a side effect.internal/gateway/websocket.Clientplaces its connection ID in the dispatch context. The client never accepts a connection identity from a payload.apps/web/lib/api/domains/queue-api.tscarries begin, renew, end, and fenced update requests.apps/web/hooks/use-queue-edit-protection.tsowns the editor lease lifecycle.queued-ghost-list.tsxandqueued-ghost-message.tsxkeep the editor attached to the selected row and identify successful saves separately from cancellations.apps/web/hooks/domains/session/use-queue.tsreconciles mutation responses and queue status for the session captured by the action. Stale HTTP/WS responses must not overwrite a newer session snapshot.
Data model and contracts
A lease is keyed by (session_id, entry_id) and contains a server-generated
lease ID, the entry's target revision at acquisition, an expiry timestamp, a
monotonic lease generation, and the server-side connection binding. Lease
connection identity is not exposed to the browser.
The queue edit end request may carry dispatch_if_auto_run: true only for a
successful save. The backend releases the validated lease first, then invokes
the policy-preserving drain for the same session. Cancel, failed save, stale
lease cleanup, and disconnect cleanup omit the field or set it false. The
response remains the existing session/entry acknowledgement; any dispatched
successor is observed through the existing queue and session events.
The queue update contract includes lease_id, operation_id, and
expected_target_revision when sent by a connected editor. The server returns
the resulting target revision and operation ID. The operation ID is scoped to
its live lease and its request content, attachments, and metadata. Repeating an
identical operation returns the original revision without applying the update
again. Reusing an operation ID with different content is rejected.
The ordinary compatibility update path remains available only for trusted non-WebSocket callers. Browser queue edits must acquire and retain a live target lease, pass its fencing fields on save, and refuse the save action when the lease is absent or has ended. The browser must never send an unfenced queue edit request.
Control flow and state transitions
- The editor requests
message.queue.edit.beginfor one visible user-owned row. The service checks the row under the session admission lock and creates one target lease without changing Auto-run. - Automatic queue advancement continues for other entries. A policy-aware head reservation skips only while the current visible head is the leased target; it may reserve earlier entries normally.
- The editor renews the lease before expiry. Each renewal checks both lease ID and connection binding and advances the lease generation.
- Save validates the lease, connection, target revision, and operation ID in one admission-serialized operation. It claims any newly referenced staged attachments only after request preconditions that can reject the save have passed. The content, attachments, and entity-reference metadata replacement then commits atomically. Superseded attachment claims are released only after a successful replacement; rejected saves release only claims made by that request.
- After a successful save, the UI ends the lease with
dispatch_if_auto_run: true. The backend validates and releases that lease before attempting the policy-preserving drain. The drain serializes against cancellation and other queue dispatches, rechecks promptability, and reserves the FIFO head only when Auto-run is already enabled. - Cancel, failed save, lease loss, disconnect cleanup, and expiry end the lease without requesting a drain. A drain, remove, merge, session transfer, or replacement that wins first invalidates the target and causes the editor to refetch instead of applying stale state.
- Delayed cleanup from the old connection is fenced by lease ID, connection ID, and session/entry key, so it cannot release a newer lease or trigger a successor turn after a later edit.
Session transfer and queue snapshot replacement must participate in the same admission boundary as editing and invalidate leases for affected source and destination entries. This prevents a transfer or restore from racing a stale save and prevents a lease from surviving under an obsolete session identity.
Failure and recovery behavior
- A second begin for the same target returns an edit conflict. A begin for a missing, reserved, or non-user row returns the non-enumerating not-found outcome.
- A stale lease, foreign connection, expired lease, or mismatched revision returns a typed edit conflict. The UI clears local edit state and fetches the authoritative queue; it does not apply an optimistic replacement.
- A duplicate operation with the same operation ID and request hash is successful and returns the original revision. A hash mismatch is rejected without a second mutation.
- Validation, lease, reference, or attachment-claim failures before the queue mutation release only attachments newly claimed by that request. Existing retained attachments remain owned by the queue.
- A requested post-save drain is never an implicit resume: Auto-run OFF, promptability guards, clarification, cancellation, or an in-flight dispatch leave the saved row pending for the next eligible trigger.
- If lease release succeeds but the post-save drain cannot dispatch, the saved row remains durable and the existing queue status event is authoritative; a later agent-ready or explicit Auto-run trigger may deliver it.
- Status events remain authoritative for every successful update. Reconciliation is scoped to the session and uses the existing request-generation guard so an older response cannot restore stale content after a newer event or session switch.
Permissions and security
Session authorization runs before queue reads for all edit actions. The server
uses its connection-bound identity rather than payload data for lease fencing.
The post-save dispatch flag is honored only when the live lease records a
successful update for that target; a browser cannot turn a cancel or failed
save into a delivery request. A browser may submit only session ID, entry ID,
lease ID, operation ID, target revision, content, and replacement metadata. It
cannot choose queued_by, task ownership, or another connection's lease.
Responsive behavior
The existing inline queue panel remains the mobile composition and its queue list remains the only internal scroll owner. Edit, save, and cancel controls remain discoverable without hover on coarse pointers, use at least 44 by 44 CSS pixel hit areas where the shared row/action pattern requires them, and do not introduce document-level horizontal overflow. Desktop keeps the compact row layout. Both surfaces use the same lease and reconciliation state.
Observability and tests
Queue edit conflicts, expired leases, rejected revisions, post-save drain
deferrals, and attachment rollback failures should retain the existing
structured queue-handler/service logging without logging message content or
attachment data. Focused backend coverage belongs beside the message-queue
service and handler tests and must cover save-after-turn-completion, Auto-run
OFF, cancel, and failed-save paths. Frontend unit coverage covers lease
lifecycle, operation fencing, and successful-save versus cancellation
completion. Playwright coverage must exercise desktop and mobile-chrome
edit/save behavior, including an Auto-run backlog and a session/view
replacement scenario.
Requirement mapping
| Requirement | Design sections |
|---|---|
REQ-UI-MESSAGE-QUEUE-MANAGEMENT-002 |
Data model and contracts; Control flow and state transitions; Failure and recovery behavior; Responsive behavior |