polygram 0.6.5 → 0.6.6
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 +14 -1
- package/lib/prompt.js +10 -14
- package/package.json +1 -1
- package/polygram.js +22 -2
package/lib/db.js
CHANGED
|
@@ -337,10 +337,23 @@ function wrap(db) {
|
|
|
337
337
|
// Dedupe check: did we already send an outbound reply to this inbound?
|
|
338
338
|
// Prevents double-processing if a redelivered/replayed message has
|
|
339
339
|
// already been answered.
|
|
340
|
+
//
|
|
341
|
+
// We also count rows in the special 'failed crashed-mid-send' state
|
|
342
|
+
// as "probably sent" for dedupe. Those rows were created when polygram
|
|
343
|
+
// crashed AFTER inserting the pending row but before marking it sent
|
|
344
|
+
// — the API call may or may not have actually reached Telegram. The
|
|
345
|
+
// boot-time markStalePending sweep flips them to 'failed' with the
|
|
346
|
+
// 'crashed-mid-send' sentinel error. Treating them as un-replied
|
|
347
|
+
// (status='sent' only) caused boot replay to re-dispatch and Telegram
|
|
348
|
+
// delivered the SAME answer twice. Treating them as replied risks the
|
|
349
|
+
// opposite (the user never got a reply because the API truly failed
|
|
350
|
+
// before reaching Telegram), but a missed reply is recoverable —
|
|
351
|
+
// the user resends — while a duplicate reply is not.
|
|
340
352
|
hasOutboundReplyTo({ chat_id, msg_id }) {
|
|
341
353
|
const row = db.prepare(`
|
|
342
354
|
SELECT 1 FROM messages
|
|
343
|
-
WHERE chat_id = ? AND direction = 'out' AND reply_to_id = ?
|
|
355
|
+
WHERE chat_id = ? AND direction = 'out' AND reply_to_id = ?
|
|
356
|
+
AND (status = 'sent' OR (status = 'failed' AND error = 'crashed-mid-send'))
|
|
344
357
|
LIMIT 1
|
|
345
358
|
`).get(chat_id, msg_id);
|
|
346
359
|
return !!row;
|
package/lib/prompt.js
CHANGED
|
@@ -32,17 +32,6 @@ function truncateReplyText(s, max = REPLY_TO_MAX_CHARS) {
|
|
|
32
32
|
return `${s.slice(0, head)}…${s.slice(-tail)}`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
* Attachment summary for reply-to (never embed full content).
|
|
37
|
-
*/
|
|
38
|
-
function summarizeReplyAttachments(attachmentsJson) {
|
|
39
|
-
if (!attachmentsJson) return '';
|
|
40
|
-
let items;
|
|
41
|
-
try { items = JSON.parse(attachmentsJson); } catch { return ''; }
|
|
42
|
-
if (!Array.isArray(items) || !items.length) return '';
|
|
43
|
-
return items.map((a) => `[${a.kind}: ${a.name}]`).join(' ');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
35
|
/**
|
|
47
36
|
* Build a reply-to block. Callers pass either:
|
|
48
37
|
* - { telegram: msg.reply_to_message } (canonical Telegram payload), or
|
|
@@ -71,15 +60,22 @@ ${xmlEscape(body)}
|
|
|
71
60
|
}
|
|
72
61
|
|
|
73
62
|
if (dbRow) {
|
|
63
|
+
// Attachment summary for the reply-to block used to read
|
|
64
|
+
// dbRow.attachments_json, but that column was dropped in migration
|
|
65
|
+
// 008. Per-attachment rows live in the `attachments` table now;
|
|
66
|
+
// building a summary here would need a separate join. For reply-to
|
|
67
|
+
// context Claude already sees the canonical Telegram payload via
|
|
68
|
+
// the `telegram` branch above (the DB-row path is only the fallback
|
|
69
|
+
// for resurrected/replayed messages where the live payload is
|
|
70
|
+
// unavailable). Skipping the summary here is acceptable — text
|
|
71
|
+
// alone is enough context for "this is what they replied to".
|
|
74
72
|
const ts = dbRow.ts ? new Date(dbRow.ts).toISOString() : '';
|
|
75
73
|
const text = truncateReplyText(dbRow.text || '');
|
|
76
|
-
const attachSummary = summarizeReplyAttachments(dbRow.attachments_json);
|
|
77
|
-
const body = [text, attachSummary].filter(Boolean).join('\n');
|
|
78
74
|
const editedAttr = dbRow.edited_ts
|
|
79
75
|
? ` edited_ts="${new Date(dbRow.edited_ts).toISOString()}"`
|
|
80
76
|
: '';
|
|
81
77
|
return `<reply_to msg_id="${dbRow.msg_id}" user="${xmlEscape(dbRow.user || 'Unknown')}" ts="${ts}"${editedAttr} source="bridge-db">
|
|
82
|
-
${xmlEscape(
|
|
78
|
+
${xmlEscape(text)}
|
|
83
79
|
</reply_to>`;
|
|
84
80
|
}
|
|
85
81
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
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
|
@@ -487,7 +487,15 @@ async function downloadAttachments(bot, token, chatId, msg, rows) {
|
|
|
487
487
|
// <attachment-failed reason="..." /> so claude tells the user
|
|
488
488
|
// "I couldn't see your <kind>" instead of pretending it received
|
|
489
489
|
// text only.
|
|
490
|
-
|
|
490
|
+
//
|
|
491
|
+
// Token redaction: the fetch URL embeds bot${TOKEN} (Telegram CDN
|
|
492
|
+
// requirement) and some undici/network error variants stringify
|
|
493
|
+
// the request including the URL into err.message. Persisting that
|
|
494
|
+
// raw to attachments.download_error or stderr would leak the bot
|
|
495
|
+
// token to anyone with DB or log access. Strip any `bot<token>`
|
|
496
|
+
// pattern from the reason before storing/logging.
|
|
497
|
+
const raw = (err.message || 'unknown').slice(0, 200);
|
|
498
|
+
const reason = raw.replace(/bot\d+:[A-Za-z0-9_-]+/g, 'bot<redacted>');
|
|
491
499
|
console.error(`[attach] download failed for ${att.name}: ${reason}`);
|
|
492
500
|
results.push({ ...att, path: null, error: reason });
|
|
493
501
|
dbWrite(() => db.markAttachmentFailed(att.id, reason),
|
|
@@ -1495,7 +1503,19 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1495
1503
|
chat_id: chatId, text: `Attachment(s) skipped: ${summary.slice(0, 300)}`,
|
|
1496
1504
|
...replyOpts(threadId),
|
|
1497
1505
|
}, { source: 'attachment-skipped', botName: BOT_NAME });
|
|
1498
|
-
} catch {
|
|
1506
|
+
} catch (err) {
|
|
1507
|
+
// Surface the failure: claude is about to reply as if the photo
|
|
1508
|
+
// was processed (because filterAttachments dropped it before
|
|
1509
|
+
// download), and the user would otherwise have no signal that
|
|
1510
|
+
// their attachment was rejected. They'd assume claude saw it
|
|
1511
|
+
// and is just answering oddly.
|
|
1512
|
+
console.error(`[${label}] failed to notify user of skipped attachments: ${err.message}`);
|
|
1513
|
+
dbWrite(() => db.logEvent('attachment-skip-notice-failed', {
|
|
1514
|
+
chat_id: chatId, msg_id: msg.message_id,
|
|
1515
|
+
error: err.message?.slice(0, 200),
|
|
1516
|
+
rejected_count: rejected.length,
|
|
1517
|
+
}), 'log attachment-skip-notice-failed');
|
|
1518
|
+
}
|
|
1499
1519
|
}
|
|
1500
1520
|
|
|
1501
1521
|
await transcribeVoiceAttachments(downloaded, {
|