bloby-bot 0.20.8 → 0.21.1

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.
@@ -20,7 +20,7 @@ import path from 'path';
20
20
  import { loadConfig } from '../../shared/config.js';
21
21
  import { WORKSPACE_DIR } from '../../shared/paths.js';
22
22
  import { log } from '../../shared/logger.js';
23
- import { startBlobyAgentQuery, type RecentMessage } from '../bloby-agent.js';
23
+ import { startBlobyAgentQuery, startConversation, pushMessage, hasConversation, type RecentMessage } from '../bloby-agent.js';
24
24
  import { WhatsAppChannel } from './whatsapp.js';
25
25
  import type { ChannelConfig, ChannelProvider, ChannelStatus, ChannelType, InboundMessage, InboundMessageAttachment, SenderRole } from './types.js';
26
26
  import type { AgentAttachment } from '../bloby-agent.js';
@@ -392,14 +392,14 @@ export class ChannelManager {
392
392
  // Show "typing..." while the agent processes
393
393
  this.startTyping(msg.channel, msg.rawSender);
394
394
 
395
- // Track text chunks for WhatsApp — send intermediate chunks when agent pauses for tool use
395
+ // Track text chunks for WhatsApp — lives for the conversation lifetime
396
396
  let waChunkBuf = '';
397
397
 
398
- startBlobyAgentQuery(
399
- convId,
400
- channelContext + msg.text,
401
- model,
402
- (type, eventData) => {
398
+ // Start a live conversation if one doesn't exist (shared with chat UI)
399
+ if (!hasConversation(convId)) {
400
+ log.info(`[channels] Starting live conversation for admin: ${convId}`);
401
+
402
+ await startConversation(convId, model, (type, eventData) => {
403
403
  // Accumulate text tokens
404
404
  if (type === 'bot:token' && eventData.token) {
405
405
  waChunkBuf += eventData.token;
@@ -414,7 +414,7 @@ export class ChannelManager {
414
414
  }
415
415
 
416
416
  if (type === 'bot:response' && eventData.content) {
417
- // Send remaining text after the last tool use (or the full response if no tools were used)
417
+ // Send remaining text
418
418
  const remaining = waChunkBuf.trim();
419
419
  if (remaining) {
420
420
  this.sendMessage(msg.channel, msg.rawSender, remaining).catch((err) => {
@@ -431,22 +431,22 @@ export class ChannelManager {
431
431
  }).catch(() => {});
432
432
  }
433
433
 
434
- // Mirror streaming + task events to chat clients
435
- if (type === 'bot:token' || type === 'bot:response' || type === 'bot:typing' || type === 'bot:tool' || type.startsWith('bot:task-')) {
436
- broadcastBloby(type, eventData);
437
- }
438
-
439
- if (type === 'bot:done' && eventData.usedFileTools) {
434
+ // Handle turn completion restart backend if needed
435
+ if (type === 'bot:turn-complete' && eventData.usedFileTools) {
440
436
  this.opts.restartBackend();
441
437
  }
442
- },
443
- agentAttachments,
444
- undefined,
445
- { botName, humanName },
446
- recentMessages,
447
- undefined, // no supportPrompt
448
- 8, // maxTurns: orchestrator mode
449
- );
438
+
439
+ // Don't forward internal events to chat clients
440
+ if (type === 'bot:turn-complete' || type === 'bot:conversation-ended') return;
441
+
442
+ // Mirror streaming + task events to chat clients
443
+ broadcastBloby(type, eventData);
444
+ }, { botName, humanName }, recentMessages);
445
+ }
446
+
447
+ // Push the message into the live conversation
448
+ const channelContent = channelContext + msg.text;
449
+ pushMessage(convId, channelContent, agentAttachments);
450
450
  }
451
451
 
452
452
  /** Handle message from a customer — runs support agent in parallel with conversation context */
@@ -441,7 +441,7 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
441
441
  <Camera className="h-4 w-4" />
442
442
  </button>
443
443
  </div>
444
- {streaming ? (
444
+ {streaming && !hasText ? (
445
445
  <button
446
446
  onClick={onStop}
447
447
  className="flex items-center justify-center h-12 w-12 shrink-0 rounded-full bg-destructive text-destructive-foreground transition-colors"
@@ -13,7 +13,12 @@ import { createWorkerApp } from '../worker/index.js';
13
13
  import { closeDb, getSession, getSetting } from '../worker/db.js';
14
14
  import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, isBackendStopping, resetBackendRestarts } from './backend.js';
15
15
  import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
16
- import { startBlobyAgentQuery, stopBlobyAgentQuery, stopSubAgentTask, type RecentMessage } from './bloby-agent.js';
16
+ import {
17
+ startConversation, pushMessage, hasConversation, endConversation,
18
+ isConversationBusy, stopSubAgentTask,
19
+ startBlobyAgentQuery, stopBlobyAgentQuery,
20
+ type RecentMessage,
21
+ } from './bloby-agent.js';
17
22
  import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js';
18
23
  import { startViteDevServers, stopViteDevServers } from './vite-dev.js';
19
24
  import { startScheduler, stopScheduler } from './scheduler.js';
@@ -1127,84 +1132,101 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
1127
1132
  }
1128
1133
  } catch {}
1129
1134
 
1130
- // Mirror chat responses to WhatsApp self-chat (if connected)
1131
- const waStatus = channelManager.getStatus('whatsapp');
1132
- const waMirrorJid = waStatus?.connected ? waStatus.info?.phoneNumber : null;
1133
- let waChunkBuf = '';
1134
-
1135
- // Start orchestrator query (maxTurns: 8 — quick tasks direct, coding delegated)
1136
1135
  log.info(`[orchestrator] ──── USER MESSAGE ────`);
1137
1136
  log.info(`[orchestrator] Content: "${content.slice(0, 100)}..."`);
1138
- log.info(`[orchestrator] Model: ${freshConfig.ai.model}`);
1139
1137
  log.info(`[orchestrator] Conv: ${convId}`);
1140
- log.info(`[orchestrator] MaxTurns: 5 (orchestrator mode)`);
1141
- agentQueryActive = true;
1142
- currentStreamConvId = convId;
1143
- currentStreamBuffer = '';
1144
- startBlobyAgentQuery(convId, content, freshConfig.ai.model, (type, eventData) => {
1145
- // Track stream buffer for reconnecting clients
1146
- if (type === 'bot:token' && eventData.token) {
1147
- currentStreamBuffer += eventData.token;
1148
- if (waMirrorJid) waChunkBuf += eventData.token;
1149
- }
1138
+ log.info(`[orchestrator] Live conversation exists: ${hasConversation(convId)}`);
1150
1139
 
1151
- // WhatsApp mirror: send intermediate chunk when agent pauses for tool use
1152
- if (type === 'bot:tool' && waMirrorJid && waChunkBuf.trim()) {
1153
- channelManager.sendMessage('whatsapp', `${waMirrorJid}@s.whatsapp.net`, waChunkBuf.trim()).catch(() => {});
1154
- waChunkBuf = '';
1155
- }
1140
+ // Start a live conversation if one doesn't exist
1141
+ if (!hasConversation(convId)) {
1142
+ log.info(`[orchestrator] Starting new live conversation...`);
1156
1143
 
1157
- // Intercept bot:doneorchestrator turn finished
1158
- if (type === 'bot:done') {
1159
- log.info(`[orchestrator] ──── TURN COMPLETE ────`);
1160
- log.info(`[orchestrator] File tools used: ${eventData.usedFileTools}`);
1161
- agentQueryActive = false;
1162
- currentStreamConvId = null;
1163
- currentStreamBuffer = '';
1164
- // Restart if agent used file tools OR file watcher detected changes during the turn
1165
- if (eventData.usedFileTools || pendingBackendRestart) {
1166
- console.log('[supervisor] Agent turn ended — restarting backend');
1167
- pendingBackendRestart = false;
1168
- if (backendRestartTimer) { clearTimeout(backendRestartTimer); backendRestartTimer = null; }
1169
- resetBackendRestarts();
1170
- stopBackend().then(() => spawnBackend(backendPort));
1171
- }
1172
- // Run deferred update if agent requested one
1173
- if (pendingUpdate) {
1174
- pendingUpdate = false;
1175
- runDeferredUpdate();
1144
+ // WhatsApp mirror state lives for the conversation lifetime
1145
+ let waChunkBuf = '';
1146
+
1147
+ await startConversation(convId, freshConfig.ai.model, (type, eventData) => {
1148
+ // Check WA mirror on each event (connection state may change)
1149
+ const waStatus = channelManager.getStatus('whatsapp');
1150
+ const waMirrorJid = waStatus?.connected ? waStatus.info?.phoneNumber : null;
1151
+
1152
+ // Track stream buffer for reconnecting clients
1153
+ if (type === 'bot:typing') {
1154
+ currentStreamConvId = convId;
1155
+ currentStreamBuffer = '';
1156
+ agentQueryActive = true;
1176
1157
  }
1177
- return; // don't forward bot:done to client
1178
- }
1179
1158
 
1180
- // Save assistant response to DB + clear stream state
1181
- if (type === 'bot:response') {
1182
- currentStreamBuffer = '';
1159
+ if (type === 'bot:token' && eventData.token) {
1160
+ currentStreamBuffer += eventData.token;
1161
+ if (waMirrorJid) waChunkBuf += eventData.token;
1162
+ }
1183
1163
 
1184
- // WhatsApp mirror: send remaining chunk
1185
- if (waMirrorJid && waChunkBuf.trim()) {
1164
+ // WhatsApp mirror: send intermediate chunk when agent pauses for tool use
1165
+ if (type === 'bot:tool' && waMirrorJid && waChunkBuf.trim()) {
1186
1166
  channelManager.sendMessage('whatsapp', `${waMirrorJid}@s.whatsapp.net`, waChunkBuf.trim()).catch(() => {});
1187
1167
  waChunkBuf = '';
1188
1168
  }
1189
1169
 
1190
- (async () => {
1191
- try {
1192
- await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
1193
- role: 'assistant', content: eventData.content, meta: { model: freshConfig.ai.model },
1194
- });
1195
- } catch (err: any) {
1196
- log.warn(`[bloby] DB persist bot response error: ${err.message}`);
1170
+ // Agent finished a turn — handle backend restart + state cleanup
1171
+ if (type === 'bot:turn-complete') {
1172
+ log.info(`[orchestrator] ──── TURN COMPLETE ────`);
1173
+ log.info(`[orchestrator] File tools used: ${eventData.usedFileTools}`);
1174
+ agentQueryActive = false;
1175
+ currentStreamConvId = null;
1176
+ currentStreamBuffer = '';
1177
+
1178
+ if (eventData.usedFileTools || pendingBackendRestart) {
1179
+ log.info('[orchestrator] Restarting backend (file tools used)');
1180
+ pendingBackendRestart = false;
1181
+ if (backendRestartTimer) { clearTimeout(backendRestartTimer); backendRestartTimer = null; }
1182
+ resetBackendRestarts();
1183
+ stopBackend().then(() => spawnBackend(backendPort));
1197
1184
  }
1198
- })();
1199
- }
1185
+ if (pendingUpdate) {
1186
+ pendingUpdate = false;
1187
+ runDeferredUpdate();
1188
+ }
1189
+ return; // don't forward to client
1190
+ }
1191
+
1192
+ // Conversation ended (query loop exited)
1193
+ if (type === 'bot:conversation-ended') {
1194
+ log.info(`[orchestrator] Conversation ended: ${convId}`);
1195
+ agentQueryActive = false;
1196
+ currentStreamConvId = null;
1197
+ currentStreamBuffer = '';
1198
+ return;
1199
+ }
1200
1200
 
1201
- // Stream all events to every connected client
1202
- // (includes bot:task-created, bot:task-progress, bot:task-done from SDK)
1203
- broadcastBloby(type, eventData);
1204
- }, data.attachments, savedFiles, { botName, humanName }, recentMessages,
1205
- undefined, // no supportPrompt
1206
- 8, // maxTurns: orchestrator mode
1207
- );
1201
+ // Save assistant response to DB
1202
+ if (type === 'bot:response') {
1203
+ currentStreamBuffer = '';
1204
+
1205
+ // WhatsApp mirror: send remaining chunk
1206
+ if (waMirrorJid && waChunkBuf.trim()) {
1207
+ channelManager.sendMessage('whatsapp', `${waMirrorJid}@s.whatsapp.net`, waChunkBuf.trim()).catch(() => {});
1208
+ waChunkBuf = '';
1209
+ }
1210
+
1211
+ (async () => {
1212
+ try {
1213
+ await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
1214
+ role: 'assistant', content: eventData.content, meta: { model: freshConfig.ai.model },
1215
+ });
1216
+ } catch (err: any) {
1217
+ log.warn(`[bloby] DB persist bot response error: ${err.message}`);
1218
+ }
1219
+ })();
1220
+ }
1221
+
1222
+ // Stream all events to every connected client
1223
+ broadcastBloby(type, eventData);
1224
+ }, { botName, humanName }, recentMessages);
1225
+ }
1226
+
1227
+ // Push the user message into the live conversation
1228
+ log.info(`[orchestrator] Pushing message into live conversation`);
1229
+ pushMessage(convId, content, data.attachments, savedFiles);
1208
1230
  })();
1209
1231
  return;
1210
1232
  }
@@ -1238,7 +1260,13 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
1238
1260
  }
1239
1261
 
1240
1262
  if (msg.type === 'user:stop') {
1241
- stopBlobyAgentQuery(convId);
1263
+ // End the live conversation (if any) or stop a one-shot query
1264
+ if (hasConversation(convId)) {
1265
+ log.info(`[orchestrator] user:stop — ending live conversation ${convId}`);
1266
+ endConversation(convId);
1267
+ } else {
1268
+ stopBlobyAgentQuery(convId);
1269
+ }
1242
1270
  return;
1243
1271
  }
1244
1272
 
@@ -1256,6 +1284,11 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
1256
1284
  if (msg.type === 'user:clear-context') {
1257
1285
  (async () => {
1258
1286
  try {
1287
+ // End the live conversation
1288
+ if (hasConversation(convId)) {
1289
+ log.info(`[orchestrator] clear-context — ending live conversation ${convId}`);
1290
+ endConversation(convId);
1291
+ }
1259
1292
  clientConvs.delete(ws);
1260
1293
  await workerApi('/api/context/clear', 'POST');
1261
1294
  } catch (err: any) {
@@ -1325,11 +1358,14 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
1325
1358
  }
1326
1359
  });
1327
1360
 
1328
- // Track whether an agent query is active — file watcher defers to bot:done during turns
1361
+ // Track whether an agent is actively processing — file watcher defers restarts during active turns
1329
1362
  let agentQueryActive = false;
1330
1363
  let pendingBackendRestart = false; // Set when file watcher fires during agent turn
1331
1364
  let pendingUpdate = false; // Set when .update file is created during agent turn
1332
1365
 
1366
+ // Note: with live conversations, agentQueryActive is true while the agent processes a message
1367
+ // and false when it's idle (waiting for next message). The live conversation stays alive between messages.
1368
+
1333
1369
  // Run bloby update as a child process.
1334
1370
  // BLOBY_SELF_UPDATE=1 tells bin/cli.js to skip daemon stop/restart —
1335
1371
  // the supervisor exits after the update finishes, and systemd (Restart=on-failure)
@@ -221,7 +221,7 @@ You handle two kinds of work differently:
221
221
  - Complex research or data gathering
222
222
  - Any coding task that touches workspace source files (client/, backend/)
223
223
 
224
- For quick tasks, use your tools directly — Read, Write, Edit, Bash. You have 5 turns, plenty for a config edit or memory write.
224
+ For quick tasks, use your tools directly — Read, Write, Edit, Bash.
225
225
 
226
226
  For coding tasks, use the Agent tool. It runs in the background — you respond immediately while the work happens behind the scenes.
227
227
 
@@ -1 +0,0 @@
1
- import{i as e}from"./bloby-C2KDOC_1.js";export{e as Mermaid};