polygram 0.6.3 → 0.6.4

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.
@@ -64,6 +64,19 @@ CREATE INDEX IF NOT EXISTS idx_attachments_unique_id
64
64
  -- mime_type, size, file_id, file_unique_id, and optionally transcription.
65
65
  -- Pre-0.5.x rows may not have file_unique_id. We pull what's there and
66
66
  -- leave the rest NULL.
67
+ --
68
+ -- Robustness: json_each() raises on malformed JSON or on a non-array
69
+ -- root, which would roll back the entire migration transaction (one bad
70
+ -- row blocks the upgrade). The `json_valid(...) AND json_type(...) =
71
+ -- 'array'` guards skip those rows so the rest still backfill. Rows
72
+ -- without a `file_id` are also skipped — the schema declares file_id
73
+ -- NOT NULL and we'd rather drop a corrupt entry than materialise a
74
+ -- permanently un-redownloadable row with file_id=''.
75
+ -- Pre-filter messages in a subquery so json_each() only runs on valid
76
+ -- JSON arrays. SQLite's json_each raises on malformed JSON or on a
77
+ -- non-array root, and that error rolls back the whole migration. The
78
+ -- subquery materialises only rows that pass json_valid + json_type
79
+ -- before the join expands them.
67
80
  INSERT INTO attachments (
68
81
  message_id, chat_id, msg_id, thread_id, bot_name,
69
82
  file_id, file_unique_id, kind, name, mime_type, size_bytes,
@@ -71,7 +84,7 @@ INSERT INTO attachments (
71
84
  )
72
85
  SELECT
73
86
  m.id, m.chat_id, m.msg_id, m.thread_id, m.bot_name,
74
- COALESCE(json_extract(att.value, '$.file_id'), ''),
87
+ json_extract(att.value, '$.file_id'),
75
88
  json_extract(att.value, '$.file_unique_id'),
76
89
  COALESCE(json_extract(att.value, '$.kind'), 'document'),
77
90
  json_extract(att.value, '$.name'),
@@ -81,9 +94,14 @@ SELECT
81
94
  'downloaded',
82
95
  json_extract(att.value, '$.transcription.text'),
83
96
  m.ts
84
- FROM messages m, json_each(m.attachments_json) att
85
- WHERE m.attachments_json IS NOT NULL
86
- AND m.direction = 'in'
87
- AND NOT EXISTS (
88
- SELECT 1 FROM attachments a WHERE a.message_id = m.id
89
- );
97
+ FROM (
98
+ SELECT id, chat_id, msg_id, thread_id, bot_name, attachments_json, ts
99
+ FROM messages
100
+ WHERE direction = 'in'
101
+ AND attachments_json IS NOT NULL
102
+ AND json_valid(attachments_json) = 1
103
+ AND json_type(attachments_json) = 'array'
104
+ AND NOT EXISTS (SELECT 1 FROM attachments a WHERE a.message_id = id)
105
+ ) m, json_each(m.attachments_json) att
106
+ WHERE json_extract(att.value, '$.file_id') IS NOT NULL
107
+ AND json_extract(att.value, '$.file_id') != '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
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
@@ -208,7 +208,13 @@ function recordInbound(msg) {
208
208
  const chatConfig = config.chats[chatId];
209
209
  const ts = (msg.date || Math.floor(Date.now() / 1000)) * 1000;
210
210
 
211
- dbWrite(() => {
211
+ // Atomic message + attachments write: all-or-nothing so a half-applied
212
+ // record can never leave a message row with zero (or partial) attachment
213
+ // rows that boot replay would silently treat as "no media." Wrapping in
214
+ // db.raw.transaction also collapses the message + N-attachment fsyncs
215
+ // into one commit (perf win for media groups: 7-attachment albums go
216
+ // from 8 sync writes to 1).
217
+ const writeInbound = db.raw.transaction(() => {
212
218
  db.insertMessage({
213
219
  chat_id: chatId,
214
220
  thread_id: threadId,
@@ -231,6 +237,13 @@ function recordInbound(msg) {
231
237
  // the upsert path; an explicit lookup is cheap and always correct.
232
238
  const messageId = db.getInboundMessageId({ chat_id: chatId, msg_id: msg.message_id });
233
239
  if (!messageId) return;
240
+ // Edit-safe insert: Telegram edited_message events re-fire
241
+ // recordInbound with the same (chat_id, msg_id). Telegram doesn't
242
+ // permit replacing media in an edit (only text/caption), so if rows
243
+ // already exist for this message_id they're correct as-is —
244
+ // re-inserting would (a) duplicate them, (b) reset download_status
245
+ // back to 'pending' and lose the local_path we already fetched.
246
+ if (db.getAttachmentsByMessage(messageId).length > 0) return;
234
247
  for (const att of attachments) {
235
248
  db.insertAttachment({
236
249
  message_id: messageId,
@@ -247,7 +260,9 @@ function recordInbound(msg) {
247
260
  ts,
248
261
  });
249
262
  }
250
- }, `insert inbound ${chatId}/${msg.message_id}`);
263
+ });
264
+
265
+ dbWrite(() => writeInbound(), `insert inbound ${chatId}/${msg.message_id}`);
251
266
  }
252
267
 
253
268