bloby-bot 0.46.2 → 0.46.3
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/chat/src/hooks/useBlobyChat.ts +4 -4
- package/supervisor/index.ts +25 -13
- package/worker/db.ts +27 -12
- package/worker/index.ts +2 -2
package/package.json
CHANGED
|
@@ -81,14 +81,14 @@ export function useBlobyChat(ws: WsClient | null, triggerReload?: number, enable
|
|
|
81
81
|
};
|
|
82
82
|
}, []);
|
|
83
83
|
|
|
84
|
-
// Load current conversation from DB (last
|
|
84
|
+
// Load current conversation from DB (last 200 messages — pagination kicks in beyond that)
|
|
85
85
|
const loadFromDb = useCallback(async () => {
|
|
86
86
|
try {
|
|
87
87
|
const ctx = await authFetch('/api/context/current').then((r) => r.json());
|
|
88
88
|
if (!ctx.conversationId) return;
|
|
89
89
|
setConversationId(ctx.conversationId);
|
|
90
90
|
|
|
91
|
-
const limit =
|
|
91
|
+
const limit = 200;
|
|
92
92
|
const res = await authFetch(`/api/conversations/${ctx.conversationId}/messages?limit=${limit}`);
|
|
93
93
|
if (!res.ok) return;
|
|
94
94
|
const data = await res.json();
|
|
@@ -100,14 +100,14 @@ export function useBlobyChat(ws: WsClient | null, triggerReload?: number, enable
|
|
|
100
100
|
} catch { /* worker not ready yet */ }
|
|
101
101
|
}, [parseMessage]);
|
|
102
102
|
|
|
103
|
-
// Load older messages (cursor-based pagination)
|
|
103
|
+
// Load older messages (cursor-based pagination — cursor is the rowid-equivalent on the server)
|
|
104
104
|
const loadOlder = useCallback(async () => {
|
|
105
105
|
if (loadingOlder.current || !conversationIdRef.current) return;
|
|
106
106
|
loadingOlder.current = true;
|
|
107
107
|
try {
|
|
108
108
|
const oldestId = messages[0]?.id;
|
|
109
109
|
if (!oldestId) return;
|
|
110
|
-
const limit =
|
|
110
|
+
const limit = 100;
|
|
111
111
|
const res = await authFetch(`/api/conversations/${conversationIdRef.current}/messages?before=${oldestId}&limit=${limit}`);
|
|
112
112
|
if (!res.ok) return;
|
|
113
113
|
const data = await res.json();
|
package/supervisor/index.ts
CHANGED
|
@@ -1327,7 +1327,11 @@ ${!connected ? `<script>
|
|
|
1327
1327
|
const data = msg.data || {};
|
|
1328
1328
|
const content = data.content;
|
|
1329
1329
|
if (!content) return;
|
|
1330
|
-
|
|
1330
|
+
// Note: we intentionally ignore data.conversationId from the client.
|
|
1331
|
+
// The server is the authority on which DB conversation this WS belongs to —
|
|
1332
|
+
// honoring a client-supplied id let stale browser state drive messages into
|
|
1333
|
+
// an orphan conv whose row had been deleted, causing FK failures on every
|
|
1334
|
+
// INSERT. Server resolution below (clientConvs → context.current → create).
|
|
1331
1335
|
|
|
1332
1336
|
// Re-read config on each message so post-onboard changes are picked up
|
|
1333
1337
|
const freshConfig = loadConfig();
|
|
@@ -1396,6 +1400,10 @@ ${!connected ? `<script>
|
|
|
1396
1400
|
});
|
|
1397
1401
|
} catch (err: any) {
|
|
1398
1402
|
log.warn(`[bloby] DB persist error: ${err.message}`);
|
|
1403
|
+
// Surface to all clients so they can flag the missing user bubble
|
|
1404
|
+
// instead of pretending it's saved. addMessage() in worker/db.ts is
|
|
1405
|
+
// self-healing for orphan convIds, so this should now be rare.
|
|
1406
|
+
broadcastBloby('chat:persist-error', { conversationId: convId, role: 'user', error: err.message });
|
|
1399
1407
|
}
|
|
1400
1408
|
|
|
1401
1409
|
// Fetch agent/user names and recent messages in parallel
|
|
@@ -1438,7 +1446,7 @@ ${!connected ? `<script>
|
|
|
1438
1446
|
// the self-chat mirror (the user's own number).
|
|
1439
1447
|
const waState = channelManager.createWaStreamState();
|
|
1440
1448
|
|
|
1441
|
-
await startConversation(convId, freshConfig.ai.model, (type, eventData) => {
|
|
1449
|
+
await startConversation(convId, freshConfig.ai.model, async (type, eventData) => {
|
|
1442
1450
|
// Track stream buffer for reconnecting clients
|
|
1443
1451
|
if (type === 'bot:typing') {
|
|
1444
1452
|
currentStreamConvId = convId;
|
|
@@ -1493,19 +1501,23 @@ ${!connected ? `<script>
|
|
|
1493
1501
|
return;
|
|
1494
1502
|
}
|
|
1495
1503
|
|
|
1496
|
-
// Save assistant response to DB
|
|
1504
|
+
// Save assistant response to DB BEFORE broadcasting so a refresh
|
|
1505
|
+
// immediately after the bubble appears can't race the INSERT and lose
|
|
1506
|
+
// the message. addMessage() in worker/db.ts is self-healing —
|
|
1507
|
+
// it INSERT OR IGNOREs the parent conversation row first, so even an
|
|
1508
|
+
// orphan convId persists cleanly.
|
|
1497
1509
|
if (type === 'bot:response') {
|
|
1498
1510
|
currentStreamBuffer = '';
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1511
|
+
try {
|
|
1512
|
+
await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
|
|
1513
|
+
role: 'assistant', content: eventData.content, meta: { model: freshConfig.ai.model },
|
|
1514
|
+
});
|
|
1515
|
+
} catch (err: any) {
|
|
1516
|
+
log.warn(`[bloby] DB persist bot response error: ${err.message}`);
|
|
1517
|
+
// Tell clients the bubble they're about to see is not durable —
|
|
1518
|
+
// they can flag/retry rather than silently losing it on refresh.
|
|
1519
|
+
broadcastBloby('chat:persist-error', { conversationId: convId, role: 'assistant', error: err.message });
|
|
1520
|
+
}
|
|
1509
1521
|
}
|
|
1510
1522
|
|
|
1511
1523
|
// Stream all events to every connected client
|
package/worker/db.ts
CHANGED
|
@@ -93,10 +93,18 @@ export function deleteConversation(id: string) {
|
|
|
93
93
|
|
|
94
94
|
// Messages
|
|
95
95
|
export function addMessage(convId: string, role: string, content: string, meta?: { tokens_in?: number; tokens_out?: number; model?: string; audio_data?: string; attachments?: string }) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
// Self-heal: if the conversation row is missing (orphan live convId, harness session
|
|
97
|
+
// drift, deleted parent, etc.), create it so the FK constraint never fires.
|
|
98
|
+
// Use the first user message as title; assistant-first stays NULL (filled by UI).
|
|
99
|
+
const tx = db.transaction(() => {
|
|
100
|
+
db.prepare('INSERT OR IGNORE INTO conversations (id, title, model) VALUES (?, ?, ?)')
|
|
101
|
+
.run(convId, role === 'user' ? content.slice(0, 80) : null, meta?.model ?? null);
|
|
102
|
+
const msg = db.prepare('INSERT INTO messages (conversation_id, role, content, tokens_in, tokens_out, model, audio_data, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *')
|
|
103
|
+
.get(convId, role, content, meta?.tokens_in ?? null, meta?.tokens_out ?? null, meta?.model ?? null, meta?.audio_data ?? null, meta?.attachments ?? null);
|
|
104
|
+
db.prepare('UPDATE conversations SET updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(convId);
|
|
105
|
+
return msg;
|
|
106
|
+
});
|
|
107
|
+
return tx() as any;
|
|
100
108
|
}
|
|
101
109
|
export function getMessages(convId: string) {
|
|
102
110
|
return db.prepare('SELECT * FROM messages WHERE conversation_id = ? ORDER BY created_at ASC').all(convId);
|
|
@@ -177,22 +185,29 @@ export function deleteAllTrustedDevices() {
|
|
|
177
185
|
db.prepare('DELETE FROM trusted_devices').run();
|
|
178
186
|
}
|
|
179
187
|
|
|
180
|
-
// Recent messages (for context injection)
|
|
188
|
+
// Recent messages (for context injection).
|
|
189
|
+
// Order by rowid (monotonic insertion order) — created_at has 1-second resolution
|
|
190
|
+
// so rapid-fire messages can collide. rowid never does.
|
|
191
|
+
// rowid is a hidden column, so `SELECT *` omits it — we must alias it explicitly
|
|
192
|
+
// in the inner query for the outer ORDER BY to reach it.
|
|
181
193
|
export function getRecentMessages(convId: string, limit = 20) {
|
|
182
194
|
return db.prepare(`
|
|
183
195
|
SELECT * FROM (
|
|
184
|
-
SELECT
|
|
185
|
-
|
|
196
|
+
SELECT messages.*, messages.rowid AS _rid FROM messages
|
|
197
|
+
WHERE conversation_id = ? ORDER BY messages.rowid DESC LIMIT ?
|
|
198
|
+
) sub ORDER BY _rid ASC
|
|
186
199
|
`).all(convId, limit);
|
|
187
200
|
}
|
|
188
201
|
|
|
189
|
-
// Cursor-based pagination: messages before a given
|
|
202
|
+
// Cursor-based pagination: messages before a given message id.
|
|
203
|
+
// Use rowid for comparison — message.id is random hex so `id < ?` is meaningless.
|
|
190
204
|
export function getMessagesBefore(convId: string, beforeId: string, limit = 20) {
|
|
191
205
|
return db.prepare(`
|
|
192
206
|
SELECT * FROM (
|
|
193
|
-
SELECT
|
|
194
|
-
WHERE conversation_id = ?
|
|
195
|
-
|
|
196
|
-
|
|
207
|
+
SELECT messages.*, messages.rowid AS _rid FROM messages
|
|
208
|
+
WHERE conversation_id = ?
|
|
209
|
+
AND messages.rowid < (SELECT rowid FROM messages WHERE id = ?)
|
|
210
|
+
ORDER BY messages.rowid DESC LIMIT ?
|
|
211
|
+
) sub ORDER BY _rid ASC
|
|
197
212
|
`).all(convId, beforeId, limit);
|
|
198
213
|
}
|
package/worker/index.ts
CHANGED
|
@@ -133,12 +133,12 @@ app.post('/api/conversations/:id/messages', (req, res) => {
|
|
|
133
133
|
});
|
|
134
134
|
app.get('/api/conversations/:id/messages/recent', (req, res) => {
|
|
135
135
|
const limit = parseInt(req.query.limit as string) || 20;
|
|
136
|
-
const msgs = getRecentMessages(req.params.id, Math.min(limit,
|
|
136
|
+
const msgs = getRecentMessages(req.params.id, Math.min(limit, 1000));
|
|
137
137
|
res.json(msgs);
|
|
138
138
|
});
|
|
139
139
|
app.get('/api/conversations/:id/messages', (req, res) => {
|
|
140
140
|
const before = req.query.before as string;
|
|
141
|
-
const limit = Math.min(parseInt(req.query.limit as string) || 20,
|
|
141
|
+
const limit = Math.min(parseInt(req.query.limit as string) || 20, 1000);
|
|
142
142
|
if (before) {
|
|
143
143
|
res.json(getMessagesBefore(req.params.id, before, limit));
|
|
144
144
|
} else {
|