feat: queued follow-up messages in composer (#58) #287
No reviewers
Labels
No labels
backend
bug
chore
duplication
effort:complex
effort:medium
effort:trivial
enhancement
frontend
fullstack
priority:high
ready-for-agent
refactor
security
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
dries/ocman!287
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/queued-follow-up-messages"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #58.
Server-side, shared, race-free follow-up message queue. Prompts submitted while a session is mid-turn are queued and auto-sent one per turn when the session goes idle.
Design
state.db(migration v21), shared across every connected client and surviving a client moving machines. A frontend-only queue would show phantom messages to other clients watching the same session.session.idleSSE edge. One message drains per idle edge, oldest first (one follow-up per turn).What's included
Backend
internal/queuesvc— enqueue / flush (per-session lock) + List/Remove/Move.internal/state— v21queued_messagetable + CRUD (append, head, list, delete, transactional reorder).POST /messageenqueues;session.idleflushes. Loops/MCP still send directly (programmatic, not composer)./api/session/{id}/queue{/qmid}, POST.../move(ownership-scoped).ocman.queue.updatedbroadcast carrying the session's full list.Frontend
useMessageQueuerenders the list purely from server state (initial fetch +queue.updatedbroadcasts).QueuedMessagesleaf component.Reliability & live-update fixes
Follow-up round hardening the live-update path so the list stays correct without a refresh:
http.Flusheron the status-recorder middleware — the wrapper implementedhttp.Hijacker(for WebSockets) but nothttp.Flusher, so the global/api/eventsSSE handler failed itsw.(http.Flusher)check and returned before subscribing. No client ever subscribed → broadcasts reached zero subscribers → the UI only updated on refresh. (Per-session/api/session/{id}/eventsworked only because it bypasses that path.) This was the root cause of "new queued messages don't appear until refresh."queue.updatedis coalesced in the broadcast hub: on a full subscriber buffer the latest snapshot is parked (last-write-wins) instead of dropped, so the freshest list is never lost under load.useMessageQueuereloads from the endpoint on every SSE (re)connect (mirroring the conversation SSE), with a monotonic seq guard so a slow reload can't clobber a fresher broadcast.platformresolving), which previously wiped a just-shown enqueue.session.idleedge never arrived.proxyResdata listener that put SSE responses into flowing mode and consumed the bytes.Tests
statusRecorderFlush/Hijack delegation.useGlobalEventsqueue routing,useMessageQueue(broadcast apply, reconnect reload, seq guard, effect-rerun survival),QueuedMessagescomponent.go test,go vet, platform-branching check,tsc -b,pnpm lint(0 errors),pnpm test(1480 pass), coverage ratchet ✅.Phase 2: surface and manage the server-side queue from the composer. - REST: GET/DELETE /api/session/{id}/queue{/qmid} and POST .../move, all ownership-scoped to the session in the URL. - queuesvc gains List/Remove/Move; state gains MoveQueuedMessage (transactional position swap) and GetQueuedMessageSession. - Frontend: useMessageQueue hook keeps a live copy in sync off the ocman.queue.updated broadcast; Composer renders the queue as a list under the footer with remove / reorder controls.Coverage ratchet: ✅ pass
Tolerance: -0.1%. Baseline stored on
gh-pages.feat: queued follow-up messages in composer (#58)to WIP: feat: queued follow-up messages in composer (#58)The status-capturing middleware wrapper implemented http.Hijacker but not http.Flusher, so the global /api/events SSE handler failed its w.(http.Flusher) check and returned before subscribing. No client ever subscribed, so broadcasts (queue.updated, etc.) reached zero subscribers and the UI only updated on refresh. Per-session /api/session/{id}/events worked only because it bypasses this path. Delegate Flush to the embedded writer, mirroring the existing Hijack.WIP: feat: queued follow-up messages in composer (#58)to feat: queued follow-up messages in composer (#58)- fixtures: mock GET /api/session/{id}/queue as an empty array. Without it useMessageQueue's fetch fell through the catch-all to the session- detail JSON (an object, not an array), breaking the queue render and detaching the composer input mid-test. - session-detail: remove the stale 'genuinely queued user message shows the Queued badge' test — the in-thread badge was removed on this branch (follow-ups now surface in the compact queue list). Sibling tests still assert the badge is absent. - vite: restore the proxyRes drain (needed to keep the e2e preview server stable) with a clarifying comment; the live-update fix is server-side (statusRecorder http.Flusher), not the proxy.103d6648b172a8d2659b