polygram 0.6.0 → 0.6.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/lib/db.js +26 -9
- package/migrations/008-drop-attachments-json.sql +12 -0
- package/package.json +1 -1
- package/polygram.js +25 -15
- package/scripts/split-db.js +23 -2
package/lib/db.js
CHANGED
|
@@ -8,7 +8,7 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const Database = require('better-sqlite3');
|
|
10
10
|
|
|
11
|
-
const SCHEMA_VERSION =
|
|
11
|
+
const SCHEMA_VERSION = 8;
|
|
12
12
|
|
|
13
13
|
function open(dbPath) {
|
|
14
14
|
const db = new Database(dbPath);
|
|
@@ -56,14 +56,16 @@ function runMigrations(db, migrationsDir) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function wrap(db) {
|
|
59
|
+
// 0.6.1: attachments_json column dropped (migration 008). All attachment
|
|
60
|
+
// data lives in the per-attachment table now (see attachments stmts below).
|
|
59
61
|
const insertMessageStmt = db.prepare(`
|
|
60
62
|
INSERT INTO messages (
|
|
61
63
|
chat_id, thread_id, msg_id, user, user_id, text, reply_to_id,
|
|
62
|
-
direction, source, bot_name,
|
|
64
|
+
direction, source, bot_name, session_id,
|
|
63
65
|
model, effort, turn_id, status, error, cost_usd, ts
|
|
64
66
|
) VALUES (
|
|
65
67
|
@chat_id, @thread_id, @msg_id, @user, @user_id, @text, @reply_to_id,
|
|
66
|
-
@direction, @source, @bot_name, @
|
|
68
|
+
@direction, @source, @bot_name, @session_id,
|
|
67
69
|
@model, @effort, @turn_id, @status, @error, @cost_usd, @ts
|
|
68
70
|
)
|
|
69
71
|
ON CONFLICT(chat_id, msg_id) DO UPDATE SET
|
|
@@ -121,8 +123,7 @@ function wrap(db) {
|
|
|
121
123
|
|
|
122
124
|
const setMessageTextStmt = db.prepare(`
|
|
123
125
|
UPDATE messages
|
|
124
|
-
SET text = @text
|
|
125
|
-
attachments_json = COALESCE(@attachments_json, attachments_json)
|
|
126
|
+
SET text = @text
|
|
126
127
|
WHERE chat_id = @chat_id AND msg_id = @msg_id
|
|
127
128
|
`);
|
|
128
129
|
|
|
@@ -174,7 +175,6 @@ function wrap(db) {
|
|
|
174
175
|
direction: row.direction || 'in',
|
|
175
176
|
source: row.source || 'polygram',
|
|
176
177
|
bot_name: row.bot_name || null,
|
|
177
|
-
attachments_json: row.attachments_json || null,
|
|
178
178
|
session_id: row.session_id || null,
|
|
179
179
|
model: row.model || null,
|
|
180
180
|
effort: row.effort || null,
|
|
@@ -240,12 +240,11 @@ function wrap(db) {
|
|
|
240
240
|
return getMessageStmt.get(String(chatId), msgId);
|
|
241
241
|
},
|
|
242
242
|
|
|
243
|
-
setMessageText({ chat_id, msg_id, text
|
|
243
|
+
setMessageText({ chat_id, msg_id, text }) {
|
|
244
244
|
return setMessageTextStmt.run({
|
|
245
245
|
chat_id: String(chat_id),
|
|
246
246
|
msg_id,
|
|
247
247
|
text: text ?? '',
|
|
248
|
-
attachments_json,
|
|
249
248
|
});
|
|
250
249
|
},
|
|
251
250
|
|
|
@@ -324,7 +323,7 @@ function wrap(db) {
|
|
|
324
323
|
const placeholders = chatIds.map(() => '?').join(',');
|
|
325
324
|
return db.prepare(`
|
|
326
325
|
SELECT id, chat_id, thread_id, msg_id, user, user_id, text, reply_to_id,
|
|
327
|
-
|
|
326
|
+
ts, handler_status
|
|
328
327
|
FROM messages
|
|
329
328
|
WHERE direction = 'in'
|
|
330
329
|
AND handler_status IN ('dispatched', 'processing', 'replay-pending')
|
|
@@ -464,6 +463,24 @@ function wrap(db) {
|
|
|
464
463
|
return db.prepare(sql).all(...args);
|
|
465
464
|
},
|
|
466
465
|
|
|
466
|
+
// Re-FK every attachment whose (chat_id, msg_id) is in `msg_ids` over
|
|
467
|
+
// to a single primary message row. Used when the media-group buffer
|
|
468
|
+
// coalesces N Telegram messages (each carrying one photo of an album)
|
|
469
|
+
// into one synthetic turn — siblings were recorded under their own
|
|
470
|
+
// msg_ids by recordInbound, but Claude needs to see them all under
|
|
471
|
+
// the primary message so handleMessage's per-message attachment
|
|
472
|
+
// lookup returns the full album.
|
|
473
|
+
reassignAttachmentsToMessage({ chat_id, msg_ids, target_message_id }) {
|
|
474
|
+
if (!Array.isArray(msg_ids) || msg_ids.length === 0) return { changes: 0 };
|
|
475
|
+
const placeholders = msg_ids.map(() => '?').join(',');
|
|
476
|
+
return db.prepare(`
|
|
477
|
+
UPDATE attachments
|
|
478
|
+
SET message_id = ?, msg_id = (SELECT msg_id FROM messages WHERE id = ?)
|
|
479
|
+
WHERE chat_id = ? AND msg_id IN (${placeholders})
|
|
480
|
+
AND message_id != ?
|
|
481
|
+
`).run(target_message_id, target_message_id, String(chat_id), ...msg_ids, target_message_id);
|
|
482
|
+
},
|
|
483
|
+
|
|
467
484
|
// Look up the messages.id auto-pk for an inbound message. Used by
|
|
468
485
|
// recordInbound to FK attachments to the just-inserted message even
|
|
469
486
|
// when an ON-CONFLICT update happened (lastInsertRowid is 0 in that
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Drop the legacy messages.attachments_json column. 0.6.0's migration 007
|
|
2
|
+
-- created the per-attachment table and backfilled from this column; the
|
|
3
|
+
-- column was kept for one minor as a safety net. polygram 0.6.1 reads
|
|
4
|
+
-- exclusively from the attachments table now, so the column is dead code
|
|
5
|
+
-- on the schema side and can go.
|
|
6
|
+
--
|
|
7
|
+
-- SQLite supports ALTER TABLE DROP COLUMN since 3.35 (well below
|
|
8
|
+
-- better-sqlite3's bundled SQLite). The op rewrites the table in place,
|
|
9
|
+
-- which is fine — `messages` is small enough that a one-time rewrite at
|
|
10
|
+
-- migration time is cheaper than carrying the column around forever.
|
|
11
|
+
|
|
12
|
+
ALTER TABLE messages DROP COLUMN attachments_json;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Telegram daemon for Claude Code that preserves the OpenClaw per-chat session model. Migration path for OpenClaw users moving to Claude Code.",
|
|
5
5
|
"main": "lib/ipc-client.js",
|
|
6
6
|
"bin": {
|
package/polygram.js
CHANGED
|
@@ -220,10 +220,6 @@ function recordInbound(msg) {
|
|
|
220
220
|
direction: 'in',
|
|
221
221
|
source: 'polygram',
|
|
222
222
|
bot_name: BOT_NAME,
|
|
223
|
-
// attachments_json kept temporarily as a fallback during the 0.6.0
|
|
224
|
-
// migration window; per-attachment rows below are the source of
|
|
225
|
-
// truth. Will be dropped in a follow-up minor.
|
|
226
|
-
attachments_json: attachments.length ? JSON.stringify(attachments) : null,
|
|
227
223
|
model: chatConfig?.model || null,
|
|
228
224
|
effort: chatConfig?.effort || null,
|
|
229
225
|
ts,
|
|
@@ -380,9 +376,7 @@ async function transcribeVoiceAttachments(downloaded, { chatId, msgId, label, bo
|
|
|
380
376
|
// parses it back when building the prompt.
|
|
381
377
|
// - Message-level: setMessageText updates messages.text with the
|
|
382
378
|
// combined transcript so FTS finds "what Maria said" via the
|
|
383
|
-
// normal chat search path.
|
|
384
|
-
// be dropped in a future minor; per-attachment row is the source
|
|
385
|
-
// of truth).
|
|
379
|
+
// normal chat search path.
|
|
386
380
|
const successful = targets.filter((a) => a.transcription?.text);
|
|
387
381
|
if (!successful.length) return;
|
|
388
382
|
for (const a of successful) {
|
|
@@ -393,8 +387,7 @@ async function transcribeVoiceAttachments(downloaded, { chatId, msgId, label, bo
|
|
|
393
387
|
}
|
|
394
388
|
const combinedText = successful.map((a) => a.transcription.text).join(' ').trim();
|
|
395
389
|
dbWrite(() => db.setMessageText({
|
|
396
|
-
chat_id: chatId, msg_id: msgId,
|
|
397
|
-
text: combinedText, attachments_json: null,
|
|
390
|
+
chat_id: chatId, msg_id: msgId, text: combinedText,
|
|
398
391
|
}), 'persist voice transcription');
|
|
399
392
|
}
|
|
400
393
|
|
|
@@ -1937,6 +1930,29 @@ function createBot(token) {
|
|
|
1937
1930
|
// all-media-no-text groups.
|
|
1938
1931
|
const primary = messages.find((m) => m.text || m.caption) || messages[0];
|
|
1939
1932
|
const merged = messages.flatMap((m) => extractAttachments(m));
|
|
1933
|
+
|
|
1934
|
+
// 0.6.0 attachment-table regression fix: recordInbound (called per
|
|
1935
|
+
// sibling on bot.on('message')) inserted each photo's row under its
|
|
1936
|
+
// OWN msg_id. handleMessage looks up attachments via
|
|
1937
|
+
// getAttachmentsByMessage(primary.message_id) — which only returns
|
|
1938
|
+
// the primary's row. Without re-FK'ing the siblings we'd silently
|
|
1939
|
+
// drop N-1 of N photos in any album, exactly the umi-assistant bug
|
|
1940
|
+
// the user hit (saw 1 of 2 photos sent in a Telegram album).
|
|
1941
|
+
const chatId = String(primary.chat.id);
|
|
1942
|
+
const primaryDbId = db.getInboundMessageId({
|
|
1943
|
+
chat_id: chatId, msg_id: primary.message_id,
|
|
1944
|
+
});
|
|
1945
|
+
const siblingMsgIds = messages
|
|
1946
|
+
.filter((m) => m.message_id !== primary.message_id)
|
|
1947
|
+
.map((m) => m.message_id);
|
|
1948
|
+
if (primaryDbId && siblingMsgIds.length) {
|
|
1949
|
+
dbWrite(() => db.reassignAttachmentsToMessage({
|
|
1950
|
+
chat_id: chatId,
|
|
1951
|
+
msg_ids: siblingMsgIds,
|
|
1952
|
+
target_message_id: primaryDbId,
|
|
1953
|
+
}), 'reassign media-group sibling attachments');
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1940
1956
|
const synthetic = { ...primary, _mergedAttachments: merged };
|
|
1941
1957
|
// Carry the primary's text verbatim (dispatchRegularMessage re-cleans
|
|
1942
1958
|
// the mention). Caption → text so downstream sees it uniformly.
|
|
@@ -2434,18 +2450,12 @@ async function main() {
|
|
|
2434
2450
|
// Attach already-recorded attachments via the media-group shortcut
|
|
2435
2451
|
// field so extractAttachments picks them up without re-parsing
|
|
2436
2452
|
// grammy fields that don't exist on this reconstructed object.
|
|
2437
|
-
// 0.6.0: read from the per-attachment table; fall back to the
|
|
2438
|
-
// legacy attachments_json blob for rows inserted before migration
|
|
2439
|
-
// 007 ran (covers the small window during the upgrade).
|
|
2440
2453
|
const attRows = db.getAttachmentsByMessage(row.id);
|
|
2441
2454
|
if (attRows.length) {
|
|
2442
2455
|
reconstructed._mergedAttachments = attRows.map((a) => ({
|
|
2443
2456
|
kind: a.kind, name: a.name, mime_type: a.mime_type,
|
|
2444
2457
|
size: a.size_bytes, file_id: a.file_id, file_unique_id: a.file_unique_id,
|
|
2445
2458
|
}));
|
|
2446
|
-
} else if (row.attachments_json) {
|
|
2447
|
-
try { reconstructed._mergedAttachments = JSON.parse(row.attachments_json); }
|
|
2448
|
-
catch {}
|
|
2449
2459
|
}
|
|
2450
2460
|
const chatConfig = config.chats[row.chat_id];
|
|
2451
2461
|
if (!chatConfig) { skipped += 1; continue; }
|
package/scripts/split-db.js
CHANGED
|
@@ -148,18 +148,39 @@ function copy(src, dst, bot, chatToBot) {
|
|
|
148
148
|
const rows = src.raw.prepare(
|
|
149
149
|
`SELECT * FROM messages WHERE chat_id IN (${ph(chatIds.length)}) OR bot_name = ?`,
|
|
150
150
|
).all(...chatIds, bot);
|
|
151
|
+
// 0.6.1: messages.attachments_json column was dropped (migration 008).
|
|
152
|
+
// Per-attachment rows live in the `attachments` table now and are
|
|
153
|
+
// copied separately below.
|
|
151
154
|
const ins = dst.raw.prepare(`
|
|
152
155
|
INSERT OR IGNORE INTO messages
|
|
153
156
|
(id, chat_id, thread_id, msg_id, user, user_id, text, reply_to_id,
|
|
154
|
-
direction, source, bot_name,
|
|
157
|
+
direction, source, bot_name, session_id,
|
|
155
158
|
model, effort, turn_id, status, error, cost_usd, ts, edited_ts)
|
|
156
159
|
VALUES
|
|
157
160
|
(@id, @chat_id, @thread_id, @msg_id, @user, @user_id, @text, @reply_to_id,
|
|
158
|
-
@direction, @source, @bot_name, @
|
|
161
|
+
@direction, @source, @bot_name, @session_id,
|
|
159
162
|
@model, @effort, @turn_id, @status, @error, @cost_usd, @ts, @edited_ts)
|
|
160
163
|
`);
|
|
161
164
|
for (const r of rows) { if (ins.run(r).changes) stats.messages++; }
|
|
162
165
|
|
|
166
|
+
// Copy per-attachment rows for the messages we just copied. FK target
|
|
167
|
+
// exists since messages were inserted in the same transaction.
|
|
168
|
+
const arows = src.raw.prepare(
|
|
169
|
+
`SELECT * FROM attachments WHERE chat_id IN (${ph(chatIds.length)})`,
|
|
170
|
+
).all(...chatIds);
|
|
171
|
+
const aIns = dst.raw.prepare(`
|
|
172
|
+
INSERT OR IGNORE INTO attachments
|
|
173
|
+
(id, message_id, chat_id, msg_id, thread_id, bot_name,
|
|
174
|
+
file_id, file_unique_id, kind, name, mime_type, size_bytes,
|
|
175
|
+
local_path, download_status, download_error, transcription, ts)
|
|
176
|
+
VALUES
|
|
177
|
+
(@id, @message_id, @chat_id, @msg_id, @thread_id, @bot_name,
|
|
178
|
+
@file_id, @file_unique_id, @kind, @name, @mime_type, @size_bytes,
|
|
179
|
+
@local_path, @download_status, @download_error, @transcription, @ts)
|
|
180
|
+
`);
|
|
181
|
+
stats.attachments = 0;
|
|
182
|
+
for (const r of arows) { if (aIns.run(r).changes) stats.attachments++; }
|
|
183
|
+
|
|
163
184
|
const srows = src.raw.prepare(`SELECT * FROM sessions WHERE chat_id IN (${ph(chatIds.length)})`).all(...chatIds);
|
|
164
185
|
const sins = dst.raw.prepare(`
|
|
165
186
|
INSERT OR REPLACE INTO sessions
|