9.7 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/ui/requirements/message-queue-send-now.md | 2026-08-05 | implemented |
Implementation Plan: Arbitrate Send Now Against FIFO Handoff
Overview
Repair the narrow race between ordinary FIFO queue draining and the explicit
Send Now operation. The diagnostic bundle shows that FIFO removes/reserves the
head and starts an asynchronous worker after the active turn becomes ready;
Send Now then sees only the in-flight marker and returns send_now_conflict,
so two queued messages run as separate turns instead of one replacement turn.
Add an explicit per-session handoff state with a pre-acceptance ownership boundary. Send Now may supersede and restore an unaccepted FIFO reservation, then claim its exact row or all-scope snapshot. Once FIFO has accepted prompt ownership, the existing fail-closed conflict remains the safe behavior. The fix is backend-only apart from a browser regression assertion; no queue schema, WebSocket shape, or frontend control change is planned.
Confirmed Root Cause
- The bundle records two queue entries, then the active turn becomes ready.
- The normal drain reserves the FIFO head and launches
executeQueuedMessageasynchronously. SendQueuedNowchecksisQueuedDispatchInFlightinqueue_send_now.goand returnsErrSendNowConflictbefore it can aggregate the queue.- The worker can also record the ordinary user message and run turn-start effects before its final prompt claim, so allowing an uncoordinated supersession would risk duplicate transcript/workflow side effects.
- The result in the bundle is two independent queued prompts followed by the Send Now error, matching this race rather than a cancellation-acknowledgement failure.
Backend
Per-session queued-dispatch handoff
Files:
apps/backend/internal/orchestrator/service.goapps/backend/internal/orchestrator/event_handlers_workflow.goapps/backend/internal/orchestrator/event_handlers_agent.goapps/backend/internal/orchestrator/task_operations.goapps/backend/internal/orchestrator/queue_send_now.go
Replace the bare session-to-entry in-flight marker with (or wrap it in) a per-session reservation that records the exact queued source and whether the automatic worker is still pre-acceptance or has claimed prompt ownership. Keep the existing per-session cancellation guard as the single arbitration point.
- Register the FIFO reservation before spawning the worker, as today.
- Make the worker claim ownership under the guard before recording an ordinary
user message, running
on_turn_starteffects, or accepting the agent prompt. - Let Send Now, while holding the same guard, supersede only a pre-acceptance FIFO reservation. Restore that exact ordinary or durable source, publish the authoritative queue state at the normal transition point, and continue through the existing exact-entry/bulk claim and replacement dispatch path.
- Preserve the existing
send_now_conflictbehavior for an accepted FIFO handoff, explicit cancellation, or another Send Now operation. Do not cancel or duplicate a successor that has already claimed the turn. - Update all marker helpers and callers (
drainQueuedMessageForPromptableSession, targeted queue takes, lifecycle dispatch checks, andpromptTask) so a stale worker cannot clear a newer owner or perform side effects after losing.
Restoration and lifecycle safety
Use the existing message-queue restoration and durable acknowledgement contract; do not add a database migration. A superseded ordinary row must return to its original FIFO position, and a durable lifecycle row must have its transient reservation cleared. The winning Send Now claim remains the only source that can create the replacement prompt and acknowledge durable sources.
Do not change explicit Cancel completion semantics, captured-turn checks,
backend-owned cancellation progress, normal FIFO ordering, or Run next
behavior outside this pre-acceptance arbitration window.
Frontend and Protocol
No production frontend or WebSocket contract change is required. The existing error mapping/refetch remains the correct response if FIFO has already accepted its successor. Extend the desktop queue E2E assertion so the aggregate appears as one user prompt, not merely as three strings that could have arrived in separate turns. Re-run the existing mobile Send Now scenario because the control and shared API path are unchanged.
Tests
- What: a normal FIFO reservation can be superseded before prompt
acceptance, restored, and included in one exact bulk Send Now claim.
File:
apps/backend/internal/orchestrator/queue_send_now_test.goor a focusedqueued_dispatch_arbitration_test.gobeside it. How: channel-synchronized handoff barriers, no sleeps; assert one aggregate prompt, both source IDs restored/claimed correctly, FIFO order, and no stale worker transcript or workflow side effects. - What: an accepted FIFO handoff remains terminal for Send Now.
File: the same focused orchestrator test file.
How: pause after ownership acceptance, issue Send Now, assert
ErrSendNowConflict, no duplicate prompt/message, no cancellation of the successor, and untouched remaining queue. - What: ordinary FIFO delivery and existing explicit-cancel/peer-interrupt
paths retain their current semantics after the handoff state change.
Files: focused existing queue/cancellation tests under
apps/backend/internal/orchestrator/. How: run the targeted regression groups with-race; include durable lifecycle and retry paths because they share the reservation marker. - What: the browser-visible bulk action produces one replacement user
prompt containing both queued bodies in order.
File:
apps/web/e2e/tests/chat/message-queue.spec.ts. How: extend the existing header Send Now scenario to assert oneuser-message-bubblecontains the aggregate, then verify the queue is empty and the turn completes. Re-run the existing mobile Send Now scenario for shared-path parity.
E2E Tests
- Scenario: while a desktop agent is busy, queue multiple messages and
activate header Send Now. Verify the replacement transcript contains one
user prompt with the concatenated bodies in FIFO order, rather than one
visible prompt per source entry, and that the queue disappears.
File:
apps/web/e2e/tests/chat/message-queue.spec.ts. - Scenario: on Pixel 5, run the existing row Send Now flow and verify the
shared arbitration result still leaves neighboring queue entries ordered,
preserves the 44px controls, and keeps document horizontal overflow at zero.
File:
apps/web/e2e/tests/chat/mobile-message-queue-management.spec.ts.
Verification Results
- RED before the arbitration implementation:
cd apps/backend && go test -race ./internal/orchestrator -run '^TestSendQueuedNowSupersedesPendingFIFOHandoff$' -count=1failed withsend-now operation is already in progress, reproducing the diagnostic-bundle race. - Focused backend:
cd apps/backend && go test -race ./internal/orchestrator -run 'Test(SendNow|QueuedDispatch|ExecuteQueuedMessage)' -count=1— 25 passed. - Queue repository:
cd apps/backend && go test -race ./internal/orchestrator/messagequeue -run 'Test.*SendNow' -count=1— 11 passed. - Broader queue/cancellation regression:
cd apps/backend && go test -race ./internal/orchestrator -run 'Test(.*Queued.*|.*Queue.*|.*Cancel.*|.*Interrupt.*|.*Peer.*)' -count=1— 179 passed. - Full orchestrator package:
cd apps/backend && go test -race ./internal/orchestrator -count=1— 1546 passed. - Changed-file Go lint:
cd apps/backend && golangci-lint run ./... --new-from-rev 5c51dde562e2264ae63544993be88fd7f383222d --timeout=5m— 0 issues. - Web lint:
cd apps && pnpm --filter @kandev/web lint— passed. - Web typecheck:
cd apps/web && pnpm run typecheck— no errors. - E2E formatting:
cd apps && pnpm exec prettier --check web/e2e/tests/chat/message-queue.spec.ts— passed. - Desktop aggregate E2E:
cd apps/web && pnpm e2e:run tests/chat/message-queue.spec.ts -- --grep 'header Send Now' --retries=0— 1 passed. - Mobile parity E2E:
cd apps/web && pnpm e2e:run --project mobile-chrome tests/chat/mobile-message-queue-management.spec.ts -- --grep 'Send Now' --retries=0— 1 passed. - Orchestrator compile check:
cd apps/backend && go test ./internal/orchestrator/... -run '^$' -count=1— no tests found; compile completed successfully. git diff --checkis run before commit.
Implementation Waves and Parallel Candidates
The default is sequential execution in the primary conversation. These waves do not authorize subagents.
Wave 1:
Wave 2:
Task 02 depends on Task 01 and is not parallel-safe with it because it verifies the changed backend timing and prompt ownership behavior.
Risks and Boundaries
- The ownership boundary must precede visible message creation and workflow turn-start effects; a late check can still create a duplicate transcript row.
- Restoring a reserved lifecycle entry must clear only the transient delivery reservation and must not acknowledge or discard the durable source.
- Once an automatic prompt is accepted, combining it with a later Send Now request is intentionally out of scope because it would require retracting or rewriting an already visible user message. The operation fails closed there.
- No schema migration, new error code, queue reordering, arbitrary subset selection, or change to normal FIFO/Run next semantics is planned.
Open Questions
None. The diagnostic evidence and existing message-queue-send-now contract
define the repair boundary.