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 +1 -1
- package/supervisor/channels/manager.ts +29 -7
- package/supervisor/index.ts +30 -7
- package/worker/db.ts +3 -0
- package/worker/index.ts +8 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
678
|
-
|
|
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]
|
|
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
|
-
}).
|
|
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,
|
package/supervisor/index.ts
CHANGED
|
@@ -1325,16 +1325,33 @@ ${!connected ? `<script>
|
|
|
1325
1325
|
}
|
|
1326
1326
|
|
|
1327
1327
|
try {
|
|
1328
|
-
//
|
|
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
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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
|
});
|