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 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 = 7;
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, attachments_json, session_id,
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, @attachments_json, @session_id,
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, attachments_json = null }) {
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
- attachments_json, ts, handler_status
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.0",
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. attachments_json is left as-is (will
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; }
@@ -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, attachments_json, session_id,
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, @attachments_json, @session_id,
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