bloby-bot 0.41.0 → 0.42.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.41.0",
3
+ "version": "0.42.0",
4
4
  "releaseNotes": [
5
5
  "1. # voice note (PTT bubble)",
6
6
  "2. # audio file + caption",
@@ -669,14 +669,25 @@ export class ChannelManager {
669
669
  const { workerApi, broadcastBloby, getModel } = this.opts;
670
670
  const model = getModel();
671
671
 
672
- // Get or create conversation (shared with chat for mirroring)
672
+ // Get or create conversation (shared with chat for mirroring).
673
+ // The current_conversation setting can desync from the DB (e.g. a chat-UI
674
+ // re-mount writing back a stale cached id, or a conv that was never
675
+ // persisted in the first place). Verify it actually exists before using it,
676
+ // otherwise we'd push messages into a ghost conv and FK-fail on every write.
673
677
  let convId: string | undefined;
674
678
  try {
675
679
  const ctx = await workerApi('/api/context/current');
676
680
  if (ctx.conversationId) {
677
- convId = ctx.conversationId;
678
- } else {
681
+ const verify = await workerApi(`/api/conversations/${ctx.conversationId}/exists`);
682
+ if (verify?.exists) {
683
+ convId = ctx.conversationId;
684
+ } else {
685
+ log.warn(`[channels] current_conversation=${ctx.conversationId} is stale (no DB row) — creating fresh`);
686
+ }
687
+ }
688
+ if (!convId) {
679
689
  const conv = await workerApi('/api/conversations', 'POST', { title: 'WhatsApp', model });
690
+ if (!conv?.id) throw new Error(`POST /api/conversations returned no id: ${JSON.stringify(conv)}`);
680
691
  convId = conv.id;
681
692
  await workerApi('/api/context/set', 'POST', { conversationId: convId });
682
693
  }
@@ -688,15 +699,20 @@ export class ChannelManager {
688
699
  // Use display text for DB/chat (hides enriched agent context from the UI)
689
700
  const displayContent = msg.displayText || msg.text;
690
701
 
691
- // Save user message to DB
702
+ // Save user message to DB. The worker returns {error,...} on failure
703
+ // without throwing, so check the response body — silent drops here are
704
+ // the original cause of "messages not appearing in UI" reports.
692
705
  try {
693
- await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
706
+ const result = await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
694
707
  role: 'user',
695
708
  content: displayContent,
696
709
  meta: { model, channel: msg.channel },
697
710
  });
711
+ if (result?.error) {
712
+ log.warn(`[channels] CRITICAL: user message NOT persisted (convId=${convId}): ${result.error}`);
713
+ }
698
714
  } catch (err: any) {
699
- log.warn(`[channels] DB persist error: ${err.message}`);
715
+ log.warn(`[channels] CRITICAL: user message NOT persisted (convId=${convId}): ${err.message}`);
700
716
  }
701
717
 
702
718
  // Broadcast to chat clients (mirroring)
@@ -764,7 +780,13 @@ export class ChannelManager {
764
780
  role: 'assistant',
765
781
  content: eventData.content,
766
782
  meta: { model },
767
- }).catch(() => {});
783
+ }).then((result: any) => {
784
+ if (result?.error) {
785
+ log.warn(`[channels] CRITICAL: assistant reply NOT persisted (convId=${convId}): ${result.error}`);
786
+ }
787
+ }).catch((err: any) => {
788
+ log.warn(`[channels] CRITICAL: assistant reply NOT persisted (convId=${convId}): ${err.message}`);
789
+ });
768
790
  }
769
791
 
770
792
  // Handle turn completion — restart backend if file tools were used,
@@ -1325,16 +1325,33 @@ ${!connected ? `<script>
1325
1325
  }
1326
1326
 
1327
1327
  try {
1328
- // Check if we have an existing conversation for this client
1328
+ // Resolve the conversation id, but verify it exists in the DB
1329
+ // before reusing it. clientConvs and current_conversation can both
1330
+ // hold stale ids (e.g. after a manual DB swap or a chat-UI
1331
+ // re-mount writing back a cached id) — pushing into a ghost conv
1332
+ // FK-fails silently and loses every message.
1329
1333
  let dbConvId = clientConvs.get(ws);
1334
+ if (dbConvId) {
1335
+ const verify = await workerApi(`/api/conversations/${dbConvId}/exists`);
1336
+ if (!verify?.exists) {
1337
+ log.warn(`[bloby] cached convId ${dbConvId} is stale (no DB row) — discarding`);
1338
+ dbConvId = undefined;
1339
+ clientConvs.delete(ws);
1340
+ }
1341
+ }
1330
1342
  if (!dbConvId) {
1331
- // Check if there's a current conversation set in settings
1332
1343
  const ctx = await workerApi('/api/context/current');
1333
1344
  if (ctx.conversationId) {
1334
- dbConvId = ctx.conversationId;
1335
- } else {
1336
- // Create a new conversation
1345
+ const verify = await workerApi(`/api/conversations/${ctx.conversationId}/exists`);
1346
+ if (verify?.exists) {
1347
+ dbConvId = ctx.conversationId;
1348
+ } else {
1349
+ log.warn(`[bloby] current_conversation=${ctx.conversationId} is stale (no DB row) — creating fresh`);
1350
+ }
1351
+ }
1352
+ if (!dbConvId) {
1337
1353
  const conv = await workerApi('/api/conversations', 'POST', { title: content.slice(0, 80), model: freshConfig.ai.model });
1354
+ if (!conv?.id) throw new Error(`POST /api/conversations returned no id: ${JSON.stringify(conv)}`);
1338
1355
  dbConvId = conv.id;
1339
1356
  await workerApi('/api/context/set', 'POST', { conversationId: dbConvId });
1340
1357
  }
@@ -1353,9 +1370,12 @@ ${!connected ? `<script>
1353
1370
  type: f.type, name: f.name, mediaType: f.mediaType, filePath: f.relPath,
1354
1371
  })));
1355
1372
  }
1356
- await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
1373
+ const result = await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
1357
1374
  role: 'user', content, meta,
1358
1375
  });
1376
+ if (result?.error) {
1377
+ log.warn(`[bloby] CRITICAL: user message NOT persisted (convId=${convId}): ${result.error}`);
1378
+ }
1359
1379
 
1360
1380
  // Broadcast user message to other clients
1361
1381
  broadcastBlobyExcept(ws, 'chat:sync', {
@@ -1467,9 +1487,12 @@ ${!connected ? `<script>
1467
1487
 
1468
1488
  (async () => {
1469
1489
  try {
1470
- await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
1490
+ const result = await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
1471
1491
  role: 'assistant', content: eventData.content, meta: { model: freshConfig.ai.model },
1472
1492
  });
1493
+ if (result?.error) {
1494
+ log.warn(`[bloby] CRITICAL: assistant reply NOT persisted (convId=${convId}): ${result.error}`);
1495
+ }
1473
1496
  } catch (err: any) {
1474
1497
  log.warn(`[bloby] DB persist bot response error: ${err.message}`);
1475
1498
  }
package/worker/db.ts CHANGED
@@ -90,6 +90,9 @@ export function listConversations(limit = 50) {
90
90
  export function deleteConversation(id: string) {
91
91
  db.prepare('DELETE FROM conversations WHERE id = ?').run(id);
92
92
  }
93
+ export function conversationExists(id: string): boolean {
94
+ return !!db.prepare('SELECT 1 FROM conversations WHERE id = ?').get(id);
95
+ }
93
96
 
94
97
  // Messages
95
98
  export function addMessage(convId: string, role: string, content: string, meta?: { tokens_in?: number; tokens_out?: number; model?: string; audio_data?: string; attachments?: string }) {
package/worker/index.ts CHANGED
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  import { loadConfig, saveConfig } from '../shared/config.js';
6
6
  import { paths, WORKSPACE_DIR } from '../shared/paths.js';
7
7
  import { log } from '../shared/logger.js';
8
- import { initDb, closeDb, listConversations, createConversation, deleteConversation, getMessages, addMessage, getSetting, getAllSettings, setSetting, createSession, getSession, deleteExpiredSessions, getRecentMessages, getMessagesBefore, addPushSubscription, removePushSubscription, getAllPushSubscriptions, getPushSubscriptionByEndpoint, createTrustedDevice, getTrustedDevice, updateDeviceLastSeen, listTrustedDevices, deleteTrustedDevice, deleteExpiredDevices, deleteAllTrustedDevices } from './db.js';
8
+ import { initDb, closeDb, listConversations, createConversation, deleteConversation, conversationExists, getMessages, addMessage, getSetting, getAllSettings, setSetting, createSession, getSession, deleteExpiredSessions, getRecentMessages, getMessagesBefore, addPushSubscription, removePushSubscription, getAllPushSubscriptions, getPushSubscriptionByEndpoint, createTrustedDevice, getTrustedDevice, updateDeviceLastSeen, listTrustedDevices, deleteTrustedDevice, deleteExpiredDevices, deleteAllTrustedDevices } from './db.js';
9
9
  import webpush from 'web-push';
10
10
  import { TOTP } from 'otpauth';
11
11
  import QRCode from 'qrcode';
@@ -120,6 +120,9 @@ app.get('/api/conversations/:id', (req, res) => {
120
120
  const msgs = getMessages(req.params.id);
121
121
  res.json({ id: req.params.id, messages: msgs });
122
122
  });
123
+ app.get('/api/conversations/:id/exists', (req, res) => {
124
+ res.json({ exists: conversationExists(req.params.id) });
125
+ });
123
126
  app.post('/api/conversations', (req, res) => {
124
127
  const { title, model } = req.body || {};
125
128
  const conv = createConversation(title, model);
@@ -128,6 +131,10 @@ app.post('/api/conversations', (req, res) => {
128
131
  app.post('/api/conversations/:id/messages', (req, res) => {
129
132
  const { role, content, meta } = req.body || {};
130
133
  if (!role || !content) { res.status(400).json({ error: 'Missing role or content' }); return; }
134
+ if (!conversationExists(req.params.id)) {
135
+ res.status(404).json({ error: 'conversation_not_found', conversationId: req.params.id });
136
+ return;
137
+ }
131
138
  const msg = addMessage(req.params.id, role, content, meta);
132
139
  res.json(msg);
133
140
  });