bloby-bot 0.48.2 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bloby-bot",
3
- "version": "0.48.2",
3
+ "version": "0.49.0",
4
4
  "releaseNotes": [
5
5
  "1. Something great..",
6
6
  "2. ",
@@ -16,6 +16,25 @@
16
16
  var intentionalClose = false;
17
17
  var originalFetch = window.fetch;
18
18
 
19
+ // Chat pub/sub state — bot:* / chat:* events forwarded from the supervisor's chat
20
+ // broadcaster. Listeners are independent of WS lifecycle: when the WS reconnects we
21
+ // re-send the subscribe message automatically.
22
+ var chatListeners = [];
23
+ var chatSubscribed = false;
24
+ var chatClientId = null;
25
+
26
+ function emitChatEvent(eventType, data) {
27
+ for (var i = 0; i < chatListeners.length; i++) {
28
+ try { chatListeners[i](eventType, data); } catch (err) { console.error('[app-ws] chat listener error', err); }
29
+ }
30
+ }
31
+
32
+ function sendChatSubscribe() {
33
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
34
+ ws.send(JSON.stringify({ type: 'chat:subscribe', data: { clientId: chatClientId } }));
35
+ console.log('[app-ws] chat:subscribe sent clientId=' + chatClientId);
36
+ }
37
+
19
38
  function buildWsUrl() {
20
39
  var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
21
40
  return proto + '//' + location.host + '/app/ws';
@@ -56,6 +75,7 @@
56
75
  reconnectDelay = RECONNECT_BASE;
57
76
  startHeartbeat();
58
77
  console.log('[app-ws] Connected to /app/ws');
78
+ if (chatSubscribed) sendChatSubscribe();
59
79
  };
60
80
 
61
81
  ws.onmessage = function (e) {
@@ -68,6 +88,16 @@
68
88
  return;
69
89
  }
70
90
 
91
+ if (msg.type === 'chat:event' && msg.data) {
92
+ emitChatEvent(msg.data.eventType, msg.data.eventData);
93
+ return;
94
+ }
95
+
96
+ if (msg.type === 'chat:subscribed') {
97
+ console.log('[app-ws] chat:subscribed ack clientId=' + (msg.data && msg.data.clientId));
98
+ return;
99
+ }
100
+
71
101
  if (msg.type === 'app:api:response' && msg.data && msg.data.id) {
72
102
  var pending = pendingRequests[msg.data.id];
73
103
  if (pending) {
@@ -194,6 +224,29 @@
194
224
  connected: function () {
195
225
  return connected;
196
226
  },
227
+ // Subscribe to chat events (bot:typing, bot:token, bot:response, chat:sync, …).
228
+ // Returns an unsubscribe function. Multiple listeners are supported. The underlying
229
+ // subscribe message is sent on first listener registration and re-sent on reconnect.
230
+ onChatEvent: function (handler) {
231
+ chatListeners.push(handler);
232
+ if (!chatSubscribed) {
233
+ chatSubscribed = true;
234
+ chatClientId =
235
+ (typeof crypto !== 'undefined' && crypto.randomUUID)
236
+ ? crypto.randomUUID()
237
+ : 'ws-' + Math.random().toString(36).slice(2);
238
+ if (ws && ws.readyState === WebSocket.OPEN) sendChatSubscribe();
239
+ }
240
+ return function unsubscribe() {
241
+ var i = chatListeners.indexOf(handler);
242
+ if (i >= 0) chatListeners.splice(i, 1);
243
+ };
244
+ },
245
+ // Stable per-tab clientId used to identify this subscriber to the server. Useful
246
+ // when calling /api/agent/chat/message so the server can skip echoing chat:sync back.
247
+ clientId: function () {
248
+ return chatClientId;
249
+ },
197
250
  };
198
251
 
199
252
  connect();
@@ -441,7 +441,21 @@ export async function startSupervisor() {
441
441
  const proxy = http.request(
442
442
  { host: '127.0.0.1', port: backendPort, path: backendPath, method: req.method, headers: req.headers },
443
443
  (proxyRes) => {
444
+ const ct = String(proxyRes.headers['content-type'] || '');
445
+ const isSse = ct.includes('text/event-stream');
446
+ if (isSse) {
447
+ console.log(`[app-proxy] SSE upstream status=${proxyRes.statusCode} ct="${ct}" url=${backendPath}`);
448
+ }
444
449
  res.writeHead(proxyRes.statusCode!, proxyRes.headers);
450
+ if (isSse) {
451
+ try { res.socket?.setNoDelay(true); } catch {}
452
+ proxyRes.on('data', (chunk: Buffer) => {
453
+ console.log(`[app-proxy] SSE chunk bytes=${chunk.length} preview=${JSON.stringify(chunk.toString('utf-8').slice(0, 80))}`);
454
+ });
455
+ proxyRes.on('end', () => console.log(`[app-proxy] SSE upstream END`));
456
+ proxyRes.on('error', (e: any) => console.log(`[app-proxy] SSE upstream ERROR ${e.message}`));
457
+ res.on('close', () => console.log(`[app-proxy] SSE res CLOSE`));
458
+ }
445
459
  proxyRes.pipe(res);
446
460
  },
447
461
  );
@@ -1243,25 +1257,33 @@ mint();
1243
1257
  // GET /api/agent/chat/stream — Server-Sent Events, every chat event (bot:*, chat:*)
1244
1258
  if (req.method === 'GET' && agentPath === '/api/agent/chat/stream') {
1245
1259
  const clientId = urlObj.searchParams.get('clientId') || undefined;
1260
+ const subId = crypto.randomBytes(8).toString('hex');
1261
+ console.log(`[sse-handler] OPEN sub=${subId} clientId=${clientId} remote=${req.socket.remoteAddress}`);
1262
+
1246
1263
  res.setHeader('Content-Type', 'text/event-stream');
1247
1264
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
1248
1265
  res.setHeader('Connection', 'keep-alive');
1249
1266
  res.setHeader('X-Accel-Buffering', 'no');
1250
1267
  res.writeHead(200);
1251
- res.write(': connected\n\n');
1268
+ try { res.socket?.setNoDelay(true); } catch {}
1269
+ const wrote = res.write(': connected\n\n');
1270
+ console.log(`[sse-handler] wrote initial comment sub=${subId} writeOk=${wrote}`);
1252
1271
 
1253
1272
  const sub: ChatSubscriber = {
1254
- id: crypto.randomBytes(8).toString('hex'),
1273
+ id: subId,
1255
1274
  clientId,
1256
1275
  send: (type, data) => {
1257
- if (res.writableEnded) return;
1258
- res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`);
1276
+ if (res.writableEnded) {
1277
+ console.log(`[sse-handler] skip write (ended) sub=${subId} type=${type}`);
1278
+ return;
1279
+ }
1280
+ const ok = res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`);
1281
+ console.log(`[sse-handler] write sub=${subId} type=${type} ok=${ok} bytes=${(type.length + JSON.stringify(data).length + 10)}`);
1259
1282
  },
1260
1283
  close: () => { try { res.end(); } catch {} },
1261
1284
  };
1262
1285
  chatSubscribers.add(sub);
1263
1286
 
1264
- // Replay current streaming state so a late-joining workspace catches up mid-turn
1265
1287
  if (agentQueryActive && currentStreamConvId) {
1266
1288
  sub.send('chat:state', {
1267
1289
  streaming: true,
@@ -1273,12 +1295,20 @@ mint();
1273
1295
  const keepAlive = setInterval(() => {
1274
1296
  if (res.writableEnded) return;
1275
1297
  res.write(': ping\n\n');
1298
+ console.log(`[sse-handler] ping sub=${subId}`);
1276
1299
  }, 25_000);
1277
1300
 
1278
1301
  req.on('close', () => {
1302
+ console.log(`[sse-handler] CLOSE sub=${subId} reason=req-close`);
1279
1303
  clearInterval(keepAlive);
1280
1304
  chatSubscribers.delete(sub);
1281
1305
  });
1306
+ res.on('close', () => {
1307
+ console.log(`[sse-handler] CLOSE sub=${subId} reason=res-close`);
1308
+ });
1309
+ res.on('error', (err: any) => {
1310
+ console.log(`[sse-handler] ERROR sub=${subId} ${err?.message}`);
1311
+ });
1282
1312
  return;
1283
1313
  }
1284
1314
 
@@ -1630,6 +1660,12 @@ mint();
1630
1660
  appWss.on('connection', (ws) => {
1631
1661
  console.log('[supervisor] App API WS client connected');
1632
1662
 
1663
+ // Per-WS chat subscription: when the client opts in, this WS joins chatSubscribers
1664
+ // and receives every bot:* / chat:* event the dashboard widget does. SSE through the
1665
+ // Cloudflare tunnel buffers chunks; WebSocket frames flow through reliably (same
1666
+ // reason /app/api fetch is routed through this WS to begin with).
1667
+ let chatSub: ChatSubscriber | null = null;
1668
+
1633
1669
  ws.on('message', (raw) => {
1634
1670
  const rawStr = raw.toString();
1635
1671
 
@@ -1645,6 +1681,44 @@ mint();
1645
1681
  return;
1646
1682
  }
1647
1683
 
1684
+ if (msg.type === 'chat:subscribe') {
1685
+ if (chatSub) chatSubscribers.delete(chatSub);
1686
+ const clientId = msg.data?.clientId;
1687
+ const subId = crypto.randomBytes(8).toString('hex');
1688
+ chatSub = {
1689
+ id: subId,
1690
+ clientId,
1691
+ send: (type, data) => {
1692
+ if (ws.readyState !== WebSocket.OPEN) return;
1693
+ ws.send(JSON.stringify({ type: 'chat:event', data: { eventType: type, eventData: data } }));
1694
+ },
1695
+ close: () => {},
1696
+ };
1697
+ chatSubscribers.add(chatSub);
1698
+ console.log(`[app-ws-chat] subscribe sub=${subId} clientId=${clientId} total=${chatSubscribers.size}`);
1699
+
1700
+ if (agentQueryActive && currentStreamConvId) {
1701
+ chatSub.send('chat:state', {
1702
+ streaming: true,
1703
+ conversationId: currentStreamConvId,
1704
+ buffer: currentStreamBuffer,
1705
+ });
1706
+ }
1707
+ if (ws.readyState === WebSocket.OPEN) {
1708
+ ws.send(JSON.stringify({ type: 'chat:subscribed', data: { clientId, subId } }));
1709
+ }
1710
+ return;
1711
+ }
1712
+
1713
+ if (msg.type === 'chat:unsubscribe') {
1714
+ if (chatSub) {
1715
+ chatSubscribers.delete(chatSub);
1716
+ console.log(`[app-ws-chat] unsubscribe sub=${chatSub.id} total=${chatSubscribers.size}`);
1717
+ chatSub = null;
1718
+ }
1719
+ return;
1720
+ }
1721
+
1648
1722
  if (msg.type !== 'app:api' || !msg.data) return;
1649
1723
 
1650
1724
  const { id, method, path: reqPath, headers: reqHeaders, body } = msg.data;
@@ -1708,6 +1782,11 @@ mint();
1708
1782
  });
1709
1783
 
1710
1784
  ws.on('close', () => {
1785
+ if (chatSub) {
1786
+ chatSubscribers.delete(chatSub);
1787
+ console.log(`[app-ws-chat] auto-unsubscribe on close sub=${chatSub.id} total=${chatSubscribers.size}`);
1788
+ chatSub = null;
1789
+ }
1711
1790
  console.log('[supervisor] App API WS client disconnected');
1712
1791
  });
1713
1792
  });
@@ -1716,11 +1795,18 @@ mint();
1716
1795
  * subscribers. The workspace mirror sees the exact same event stream the widget does. */
1717
1796
  function broadcastBloby(type: string, data: any = {}) {
1718
1797
  const msg = JSON.stringify({ type, data });
1798
+ let wsCount = 0;
1719
1799
  for (const client of blobyWss.clients) {
1720
- if (client.readyState === WebSocket.OPEN) client.send(msg);
1800
+ if (client.readyState === WebSocket.OPEN) { client.send(msg); wsCount++; }
1721
1801
  }
1802
+ let sseCount = 0;
1722
1803
  for (const sub of chatSubscribers) {
1723
- try { sub.send(type, data); } catch {}
1804
+ try { sub.send(type, data); sseCount++; } catch (e: any) {
1805
+ console.log(`[sse-broadcast] send failed sub=${sub.id}: ${e.message}`);
1806
+ }
1807
+ }
1808
+ if (type.startsWith('bot:') || type.startsWith('chat:')) {
1809
+ console.log(`[sse-broadcast] type=${type} ws=${wsCount} sse=${sseCount} subs=${chatSubscribers.size}`);
1724
1810
  }
1725
1811
  }
1726
1812