feat: cut relay tunnel bandwidth with compression and SSE replay

Relay-metered traffic drops on every channel that carried avoidable
bytes; binary payloads (attachments, voice audio) are unchanged.

Hub:
- gzip /api/* JSON responses, gated by q-aware Accept-Encoding
  negotiation (explicit gzip;q=0 beats a wildcard in either order;
  hono's compress() alone matches by substring)
- enable WebSocket permessage-deflate and default flagless ws.send()
  to compressed frames - Bun negotiates the extension but compresses
  nothing unless each send opts in, and @socket.io/bun-engine never
  passes the flag (measured 96 KB terminal payload -> 608 B on wire)
- replay missed SSE events on reconnect: 256-event/2MB ring buffer,
  per-process epoch ids bound to the authenticated namespace so a
  token swap can never resume from a foreign cursor, standard
  Last-Event-ID header preferred over the ?lastEventId fallback,
  live broadcasts queued until the replay flushes to preserve order

Web:
- skip the full sessions/details/messages resync when the hub answers
  resume:ok - a phone unlock now costs a handshake plus the gap delta
  instead of refetching everything
- own every EventSource retry path: take over browser-native
  CONNECTING retries, defer reconnects while the tab is hidden, and
  raise the backoff ceiling to 5 min after repeated failures
- drop the 30s skills/slash-commands polling; refresh on demand
  without blocking the suggestion menu behind a stalled CLI RPC
- remember the websocket upgrade across terminal socket reconnects
This commit is contained in:
weishu
2026-08-05 14:59:31 +08:00
parent 27bc6bade3
commit 46fc83d211
16 changed files with 905 additions and 51 deletions
+8 -1
View File
@@ -555,7 +555,14 @@ export const SyncEventSchema = z.discriminatedUnion('type', [
type: z.literal('connection-changed'),
data: z.object({
status: z.string(),
subscriptionId: z.string().optional()
subscriptionId: z.string().optional(),
/**
* Reconnect verdict. 'ok' means the hub replayed every event the
* client missed (sent right after this one), so the client can skip
* its full refetch. 'gap' (or absence, on older hubs) means the
* client must resync from REST.
*/
resume: z.enum(['ok', 'gap']).optional()
}).optional()
})
])