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
follow-up
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 ✅.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)WIP: feat: queued follow-up messages in composer (#58)to feat: queued follow-up messages in composer (#58)103d6648b172a8d2659b