fix(queue): reconcile missed delivery across live transports (#3440)

Deliver queue updates on the control SSE stream, recover independently of bootstrap suppression, and preserve authoritative empty snapshots against delayed responses. Coalesce hydration and recovery while retaining unfinished legacy migration across runtime switches.

Workspace type-check, lint, tests and build passed. Follow-up recovery and migration fixes pass 27 queue tests, 3 control-stream tests and UI type-check.
This commit is contained in:
Bohdan Triapitsyn
2026-09-09 18:19:23 +03:00
10 changed files with 386 additions and 76 deletions
+7 -1
View File
@@ -913,7 +913,13 @@ const messageQueueRuntime = createMessageQueueRuntime({
buildOpenCodeUrl,
getOpenCodeAuthHeaders,
sessionKnowledgeRuntime,
broadcastGlobalUiEvent,
// OpenCode's /global/event SSE proxy cannot carry OpenChamber-owned events.
// Use the shared control stream for SSE clients and the existing WS fan-out.
broadcastGlobalUiEvent: createGlobalUiEventBroadcaster({
sseClients: uiOpenChamberEventClients,
wsClients: uiNotificationWsClients,
writeSseEvent,
}),
onPromptSent: (sessionId) => sessionRuntime.markUserMessageSent(sessionId),
dataDir: OPENCHAMBER_DATA_DIR,
});
@@ -140,7 +140,10 @@ allowlists.
Every mutation broadcasts `openchamber:message-queue.updated` with
`{ revision, session }` to all connected clients (SSE and WS), so several
devices on one server see one queue. The session in that payload always names
devices on one server see one queue. SSE uses the shared control stream at
`/api/openchamber/events`; `/api/global/event` carries no OpenChamber events.
The UI subscribes independently of its OpenCode transport and re-reads the
snapshot whenever either stream reconnects. The session in that payload always names
its `directory`, including the broadcast that removes the last item: the UI
keys its projection by directory, and a broadcast without one left the
delivered message on screen (a session's directory is remembered until the