polygram 0.6.8 → 0.6.10
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/polygram.js +102 -73
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.10",
|
|
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
|
@@ -200,6 +200,15 @@ function dbWrite(fn, context) {
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
// Convenience for the most common dbWrite pattern: log an event.
|
|
204
|
+
// Pre-0.6.9 every call site was logEvent(KIND, {...}),
|
|
205
|
+
// `log ${KIND}`) — three repeated lines for one logical operation.
|
|
206
|
+
// This collapses them to logEvent(KIND, {...}). Same best-effort
|
|
207
|
+
// semantics; never throws.
|
|
208
|
+
function logEvent(kind, detail) {
|
|
209
|
+
logEvent(kind, detail), `log ${kind}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
203
212
|
function recordInbound(msg) {
|
|
204
213
|
const chatId = msg.chat.id.toString();
|
|
205
214
|
const threadId = msg.message_thread_id?.toString() || null;
|
|
@@ -374,17 +383,17 @@ async function transcribeVoiceAttachments(downloaded, { chatId, msgId, label, bo
|
|
|
374
383
|
const r = await transcribeVoice(a.path, opts);
|
|
375
384
|
a.transcription = r;
|
|
376
385
|
console.log(`[${label}] transcribed ${a.kind} (${r.duration_sec?.toFixed?.(1) || '?'}s, ${r.text.length} chars)`);
|
|
377
|
-
|
|
386
|
+
logEvent('voice-transcribed', {
|
|
378
387
|
chat_id: chatId, msg_id: msgId,
|
|
379
388
|
provider: r.provider, language: r.language,
|
|
380
389
|
duration_sec: r.duration_sec, chars: r.text.length,
|
|
381
390
|
cost_usd: r.cost_usd,
|
|
382
|
-
})
|
|
391
|
+
});
|
|
383
392
|
} catch (err) {
|
|
384
393
|
console.error(`[${label}] transcribe failed for ${a.name}: ${err.message}`);
|
|
385
|
-
|
|
394
|
+
logEvent('voice-transcribe-failed', {
|
|
386
395
|
chat_id: chatId, msg_id: msgId, name: a.name, error: err.message,
|
|
387
|
-
})
|
|
396
|
+
});
|
|
388
397
|
}
|
|
389
398
|
}));
|
|
390
399
|
|
|
@@ -415,7 +424,13 @@ async function transcribeVoiceAttachments(downloaded, { chatId, msgId, label, bo
|
|
|
415
424
|
// downloads are capped to a small pool. Telegram's per-bot rate limit is
|
|
416
425
|
// ~30 req/s, so 6 concurrent fetches is comfortably under and keeps the
|
|
417
426
|
// happy path responsive without burning sockets on a 100-file edge case.
|
|
418
|
-
|
|
427
|
+
// Override via `config.bot.attachmentConcurrency` (per-bot) for ops-time
|
|
428
|
+
// rate-limit tuning.
|
|
429
|
+
const ATTACHMENT_DOWNLOAD_CONCURRENCY_DEFAULT = 6;
|
|
430
|
+
function attachmentConcurrency() {
|
|
431
|
+
const v = Number(config.bot?.attachmentConcurrency);
|
|
432
|
+
return (Number.isInteger(v) && v > 0) ? v : ATTACHMENT_DOWNLOAD_CONCURRENCY_DEFAULT;
|
|
433
|
+
}
|
|
419
434
|
|
|
420
435
|
// Per-attachment download. Pure function over (att, deps) → result. Pulled
|
|
421
436
|
// out of the loop so downloadAttachments can run several in parallel.
|
|
@@ -519,7 +534,7 @@ async function downloadAttachments(bot, token, chatId, msg, rows) {
|
|
|
519
534
|
const results = new Array(rows.length);
|
|
520
535
|
let cursor = 0;
|
|
521
536
|
const workers = Array.from(
|
|
522
|
-
{ length: Math.min(
|
|
537
|
+
{ length: Math.min(attachmentConcurrency(), rows.length) },
|
|
523
538
|
async () => {
|
|
524
539
|
while (true) {
|
|
525
540
|
const idx = cursor++;
|
|
@@ -679,7 +694,14 @@ async function sendToProcess(sessionKey, prompt, context = {}) {
|
|
|
679
694
|
// - emit a `queue-depth-warning` event if the count ever exceeds a
|
|
680
695
|
// threshold (abnormal inbound rate, slow pre-work, stuck bot)
|
|
681
696
|
// - (future) drain on shutdown if we want clean exit
|
|
682
|
-
|
|
697
|
+
// Threshold for the queue-depth-warning event (operator-tuned signal
|
|
698
|
+
// for "this session is getting hammered"). Override via
|
|
699
|
+
// `config.bot.queueWarnThreshold`. Below the threshold no event fires.
|
|
700
|
+
const CONCURRENT_WARN_THRESHOLD_DEFAULT = 20;
|
|
701
|
+
function queueWarnThreshold() {
|
|
702
|
+
const v = Number(config.bot?.queueWarnThreshold);
|
|
703
|
+
return (Number.isInteger(v) && v > 0) ? v : CONCURRENT_WARN_THRESHOLD_DEFAULT;
|
|
704
|
+
}
|
|
683
705
|
const inFlightHandlers = new Map(); // sessionKey → count
|
|
684
706
|
|
|
685
707
|
// Set true by the SIGTERM/SIGINT handler. Module-scoped so the
|
|
@@ -744,11 +766,12 @@ function isSessionRecentlyAborted(sessionKey) {
|
|
|
744
766
|
function dispatchHandleMessage(sessionKey, chatId, msg, bot) {
|
|
745
767
|
const count = (inFlightHandlers.get(sessionKey) || 0) + 1;
|
|
746
768
|
inFlightHandlers.set(sessionKey, count);
|
|
747
|
-
|
|
748
|
-
|
|
769
|
+
const warnAt = queueWarnThreshold();
|
|
770
|
+
if (count === warnAt) {
|
|
771
|
+
logEvent('queue-depth-warning', {
|
|
749
772
|
chat_id: chatId, session_key: sessionKey,
|
|
750
|
-
in_flight: count, threshold:
|
|
751
|
-
})
|
|
773
|
+
in_flight: count, threshold: warnAt,
|
|
774
|
+
});
|
|
752
775
|
}
|
|
753
776
|
handleMessage(sessionKey, chatId, msg, bot).catch((err) => {
|
|
754
777
|
const wasAborted = isSessionRecentlyAborted(sessionKey);
|
|
@@ -761,14 +784,14 @@ function dispatchHandleMessage(sessionKey, chatId, msg, bot) {
|
|
|
761
784
|
chat_id: chatId, msg_id: msg.message_id,
|
|
762
785
|
status: wasAborted ? 'aborted' : 'failed',
|
|
763
786
|
}), 'set handler_status=failed/aborted');
|
|
764
|
-
|
|
787
|
+
logEvent('handler-error', {
|
|
765
788
|
chat_id: chatId, session_key: sessionKey,
|
|
766
789
|
msg_id: msg?.message_id,
|
|
767
790
|
error: err.message?.slice(0, 500),
|
|
768
791
|
stack: err.stack?.split('\n').slice(0, 5).join('\n'),
|
|
769
792
|
aborted: wasAborted || undefined,
|
|
770
793
|
replay: isReplay || undefined,
|
|
771
|
-
})
|
|
794
|
+
});
|
|
772
795
|
// Suppress the user-facing error reply when:
|
|
773
796
|
// - boot replay (user typed this minutes ago and moved on)
|
|
774
797
|
// - polygram is shutting down (the failure is "Process killed" /
|
|
@@ -1081,11 +1104,11 @@ async function handleApprovalCallback(ctx) {
|
|
|
1081
1104
|
return;
|
|
1082
1105
|
}
|
|
1083
1106
|
if (!approvalTokensEqual(row.callback_token, token)) {
|
|
1084
|
-
|
|
1107
|
+
logEvent('approval-token-mismatch', {
|
|
1085
1108
|
id, from_user: ctx.from?.id,
|
|
1086
1109
|
// Don't log the sent_token — attackers guessing it don't need to know
|
|
1087
1110
|
// which prefix they got close on.
|
|
1088
|
-
})
|
|
1111
|
+
});
|
|
1089
1112
|
await ctx.answerCallbackQuery({ text: 'Bad token.', show_alert: true }).catch(() => {});
|
|
1090
1113
|
return;
|
|
1091
1114
|
}
|
|
@@ -1099,9 +1122,9 @@ async function handleApprovalCallback(ctx) {
|
|
|
1099
1122
|
const apprCfg = config.bot?.approvals;
|
|
1100
1123
|
const expectedChat = String(apprCfg?.adminChatId || '');
|
|
1101
1124
|
if (String(ctx.chat?.id) !== expectedChat) {
|
|
1102
|
-
|
|
1125
|
+
logEvent('approval-foreign-chat', {
|
|
1103
1126
|
id, from_chat: ctx.chat?.id, expected: expectedChat,
|
|
1104
|
-
})
|
|
1127
|
+
});
|
|
1105
1128
|
await ctx.answerCallbackQuery({ text: 'Not authorised here.', show_alert: true }).catch(() => {});
|
|
1106
1129
|
return;
|
|
1107
1130
|
}
|
|
@@ -1125,9 +1148,9 @@ async function handleApprovalCallback(ctx) {
|
|
|
1125
1148
|
}).catch(() => {});
|
|
1126
1149
|
return;
|
|
1127
1150
|
}
|
|
1128
|
-
|
|
1151
|
+
logEvent('approval-resolved', {
|
|
1129
1152
|
id, status, by: userId, user, bot: BOT_NAME,
|
|
1130
|
-
})
|
|
1153
|
+
});
|
|
1131
1154
|
|
|
1132
1155
|
// Edit the card to show the decision.
|
|
1133
1156
|
try {
|
|
@@ -1188,7 +1211,7 @@ async function handleConfigCallback(ctx) {
|
|
|
1188
1211
|
chat_id: chatId, thread_id: null, field: setting,
|
|
1189
1212
|
old_value: oldValue, new_value: value,
|
|
1190
1213
|
user: cmdUser, user_id: cmdUserId, source: 'inline-button',
|
|
1191
|
-
})
|
|
1214
|
+
});
|
|
1192
1215
|
|
|
1193
1216
|
// Graceful respawn of the topic's session that the card is in. With
|
|
1194
1217
|
// isolateTopics=false sessionKey is the chat (one shared session). With
|
|
@@ -1243,16 +1266,16 @@ function startApprovalSweeper(intervalMs = 30_000) {
|
|
|
1243
1266
|
// Silent failure here is invisible death — pending approvals time out
|
|
1244
1267
|
// with no operator signal. Log loudly.
|
|
1245
1268
|
console.error(`[approvals] sweeper DB error: ${err.message}`);
|
|
1246
|
-
|
|
1269
|
+
logEvent('approval-sweep-failed', {
|
|
1247
1270
|
error: err.message?.slice(0, 300),
|
|
1248
|
-
})
|
|
1271
|
+
});
|
|
1249
1272
|
return;
|
|
1250
1273
|
}
|
|
1251
1274
|
for (const row of rows) {
|
|
1252
1275
|
approvals.resolve({ id: row.id, status: 'timeout' });
|
|
1253
|
-
|
|
1276
|
+
logEvent('approval-timeout', {
|
|
1254
1277
|
id: row.id, bot: BOT_NAME, tool: row.tool_name,
|
|
1255
|
-
})
|
|
1278
|
+
});
|
|
1256
1279
|
resolveApprovalWaiter(row.id, 'timeout', 'swept');
|
|
1257
1280
|
// Best-effort: edit the card to show the timeout. Routed through
|
|
1258
1281
|
// tg() so the edit gets the same plain-text formatting policy as
|
|
@@ -1364,7 +1387,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1364
1387
|
chat_id: chatId, thread_id: threadIdStr, field: 'model',
|
|
1365
1388
|
old_value: oldModel, new_value: newModel,
|
|
1366
1389
|
user: cmdUser, user_id: cmdUserId, source: 'command',
|
|
1367
|
-
})
|
|
1390
|
+
});
|
|
1368
1391
|
const { anyActive } = requestRespawnForSession('model-change');
|
|
1369
1392
|
const ver = MODEL_VERSIONS[newModel] || newModel;
|
|
1370
1393
|
const suffix = anyActive ? ` — I'll switch when I finish` : '';
|
|
@@ -1384,7 +1407,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1384
1407
|
chat_id: chatId, thread_id: threadIdStr, field: 'effort',
|
|
1385
1408
|
old_value: oldEffort, new_value: newEffort,
|
|
1386
1409
|
user: cmdUser, user_id: cmdUserId, source: 'command',
|
|
1387
|
-
})
|
|
1410
|
+
});
|
|
1388
1411
|
const { anyActive } = requestRespawnForSession('effort-change');
|
|
1389
1412
|
const suffix = anyActive ? ` — I'll switch when I finish` : '';
|
|
1390
1413
|
await sendReply(`Effort → ${newEffort}${suffix}`);
|
|
@@ -1414,10 +1437,10 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1414
1437
|
ttlMs: args.ttl ? parsePairingTtl(args.ttl) : undefined,
|
|
1415
1438
|
note: args.note || null,
|
|
1416
1439
|
});
|
|
1417
|
-
|
|
1440
|
+
logEvent('pair-code-issued', {
|
|
1418
1441
|
bot: BOT_NAME, by: issuerId, scope: out.scope,
|
|
1419
1442
|
chat_id: out.chat_id, note: out.note,
|
|
1420
|
-
})
|
|
1443
|
+
});
|
|
1421
1444
|
const ttlLabel = args.ttl || '10m';
|
|
1422
1445
|
const chatLabel = out.chat_id ? `chat ${out.chat_id}` : 'any chat';
|
|
1423
1446
|
await sendReply(
|
|
@@ -1449,9 +1472,9 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1449
1472
|
await sendReply('Usage: /unpair <user_id>'); return;
|
|
1450
1473
|
}
|
|
1451
1474
|
const n = pairings.revokeByUser({ bot_name: BOT_NAME, user_id: targetId });
|
|
1452
|
-
|
|
1475
|
+
logEvent('pair-revoked', {
|
|
1453
1476
|
bot: BOT_NAME, user_id: targetId, by: cmdUserId, count: n,
|
|
1454
|
-
})
|
|
1477
|
+
});
|
|
1455
1478
|
await sendReply(n ? `Revoked ${n} pairing(s) for user ${targetId}.` : `No active pairings for user ${targetId}.`);
|
|
1456
1479
|
return;
|
|
1457
1480
|
}
|
|
@@ -1463,10 +1486,10 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1463
1486
|
code, claimer_user_id: cmdUserId,
|
|
1464
1487
|
chat_id: chatId, bot_name: BOT_NAME,
|
|
1465
1488
|
});
|
|
1466
|
-
|
|
1489
|
+
logEvent('pair-claim-attempt', {
|
|
1467
1490
|
bot: BOT_NAME, user_id: cmdUserId, chat_id: chatId,
|
|
1468
1491
|
ok: res.ok, reason: res.reason,
|
|
1469
|
-
})
|
|
1492
|
+
});
|
|
1470
1493
|
if (res.ok) {
|
|
1471
1494
|
const chatLabel = res.chat_id ? `chat ${res.chat_id}` : `every chat ${BOT_NAME} is in`;
|
|
1472
1495
|
await sendReply(`Paired. You can use me in ${chatLabel}.${res.note ? `\n(${res.note})` : ''}`);
|
|
@@ -1492,7 +1515,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1492
1515
|
const { accepted, rejected } = filterAttachments(rawAtts);
|
|
1493
1516
|
for (const { att, reason } of rejected) {
|
|
1494
1517
|
console.log(`[${label}] attachment skipped: ${att.name} (${reason})`);
|
|
1495
|
-
|
|
1518
|
+
logEvent('attachment-skipped', { chat_id: chatId, msg_id: msg.message_id, name: att.name, reason });
|
|
1496
1519
|
}
|
|
1497
1520
|
const token = config.bot?.token || '';
|
|
1498
1521
|
|
|
@@ -1542,11 +1565,11 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1542
1565
|
// their attachment was rejected. They'd assume claude saw it
|
|
1543
1566
|
// and is just answering oddly.
|
|
1544
1567
|
console.error(`[${label}] failed to notify user of skipped attachments: ${err.message}`);
|
|
1545
|
-
|
|
1568
|
+
logEvent('attachment-skip-notice-failed', {
|
|
1546
1569
|
chat_id: chatId, msg_id: msg.message_id,
|
|
1547
1570
|
error: err.message?.slice(0, 200),
|
|
1548
1571
|
rejected_count: rejected.length,
|
|
1549
|
-
})
|
|
1572
|
+
});
|
|
1550
1573
|
}
|
|
1551
1574
|
}
|
|
1552
1575
|
|
|
@@ -1558,9 +1581,9 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1558
1581
|
const stopTyping = startTyping({
|
|
1559
1582
|
bot, chatId, threadId,
|
|
1560
1583
|
logger: { error: (m) => console.error(`[${label}] ${m}`) },
|
|
1561
|
-
onEvent: (e) =>
|
|
1584
|
+
onEvent: (e) => logEvent(e.kind, {
|
|
1562
1585
|
bot: BOT_NAME, chat_id: e.chat_id, ...(e.detail || {}),
|
|
1563
|
-
}),
|
|
1586
|
+
}),
|
|
1564
1587
|
});
|
|
1565
1588
|
|
|
1566
1589
|
const botCfg = config.bot || {};
|
|
@@ -1603,11 +1626,11 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1603
1626
|
// Stream-edit failures would otherwise be invisible — edits
|
|
1604
1627
|
// don't insert a messages row by default (tg() does, but we
|
|
1605
1628
|
// want the failure path specifically surfaced). Log to events.
|
|
1606
|
-
|
|
1629
|
+
logEvent('telegram-edit-failed', {
|
|
1607
1630
|
chat_id: chatId, msg_id: messageId,
|
|
1608
1631
|
api_error: err.message?.slice(0, 200),
|
|
1609
1632
|
bot: BOT_NAME,
|
|
1610
|
-
})
|
|
1633
|
+
});
|
|
1611
1634
|
throw err;
|
|
1612
1635
|
}
|
|
1613
1636
|
},
|
|
@@ -1842,10 +1865,10 @@ function createBot(token) {
|
|
|
1842
1865
|
code, claimer_user_id: userId,
|
|
1843
1866
|
chat_id: chatId, bot_name: BOT_NAME,
|
|
1844
1867
|
});
|
|
1845
|
-
|
|
1868
|
+
logEvent('pair-claim-attempt', {
|
|
1846
1869
|
bot: BOT_NAME, user_id: userId, chat_id: chatId,
|
|
1847
1870
|
ok: res.ok, reason: res.reason, via: 'auto-onboard',
|
|
1848
|
-
})
|
|
1871
|
+
});
|
|
1849
1872
|
|
|
1850
1873
|
if (!res.ok) {
|
|
1851
1874
|
const reply = res.reason === 'rate-limited'
|
|
@@ -1865,10 +1888,10 @@ function createBot(token) {
|
|
|
1865
1888
|
|
|
1866
1889
|
const cwd = paired.cwd || firstChat.cwd;
|
|
1867
1890
|
if (!cwd) {
|
|
1868
|
-
|
|
1891
|
+
logEvent('auto-onboard-failed', {
|
|
1869
1892
|
bot: BOT_NAME, chat_id: chatId, user_id: userId,
|
|
1870
1893
|
reason: 'no-cwd',
|
|
1871
|
-
})
|
|
1894
|
+
});
|
|
1872
1895
|
await send('Paired, but no working directory is configured. Ask the operator to set pairedChatDefaults.cwd.');
|
|
1873
1896
|
return null;
|
|
1874
1897
|
}
|
|
@@ -1889,10 +1912,10 @@ function createBot(token) {
|
|
|
1889
1912
|
catch (err) {
|
|
1890
1913
|
console.error(`[${BOT_NAME}] saveConfig on auto-onboard failed: ${err.message}`);
|
|
1891
1914
|
}
|
|
1892
|
-
|
|
1915
|
+
logEvent('chat-auto-created', {
|
|
1893
1916
|
bot: BOT_NAME, chat_id: chatId, user_id: userId,
|
|
1894
1917
|
source: 'pair-claim', model: newChat.model, effort: newChat.effort,
|
|
1895
|
-
})
|
|
1918
|
+
});
|
|
1896
1919
|
|
|
1897
1920
|
const chatLabel = res.chat_id ? `chat ${res.chat_id}` : `every chat ${BOT_NAME} is in`;
|
|
1898
1921
|
const suffix = res.note ? `\n(${res.note})` : '';
|
|
@@ -1935,11 +1958,11 @@ function createBot(token) {
|
|
|
1935
1958
|
// same as killing the chat — behavior unchanged for the common case.
|
|
1936
1959
|
await pm.kill(sessionKey).catch((err) =>
|
|
1937
1960
|
console.error(`[${BOT_NAME}] abort kill failed: ${err.message}`));
|
|
1938
|
-
|
|
1961
|
+
logEvent('abort-requested', {
|
|
1939
1962
|
chat_id: chatId, user_id: msg.from?.id || null,
|
|
1940
1963
|
had_active: hadActive,
|
|
1941
1964
|
trigger: cleanText.slice(0, 40),
|
|
1942
|
-
})
|
|
1965
|
+
});
|
|
1943
1966
|
// Reply in the same language the user aborted in. Cyrillic-detection
|
|
1944
1967
|
// is crude but reliable for ru/en (the only two cue sets we ship).
|
|
1945
1968
|
const lang = /[а-яё]/i.test(cleanText) ? 'ru' : 'en';
|
|
@@ -2037,11 +2060,11 @@ function createBot(token) {
|
|
|
2037
2060
|
|
|
2038
2061
|
bot.on('message', async (ctx) => {
|
|
2039
2062
|
if (!isWellFormedMessage(ctx.message)) {
|
|
2040
|
-
|
|
2063
|
+
logEvent('malformed-update', {
|
|
2041
2064
|
bot: BOT_NAME,
|
|
2042
2065
|
update_id: ctx.update?.update_id,
|
|
2043
2066
|
reason: 'missing chat.id / message_id',
|
|
2044
|
-
})
|
|
2067
|
+
});
|
|
2045
2068
|
return;
|
|
2046
2069
|
}
|
|
2047
2070
|
const chatId = ctx.chat.id.toString();
|
|
@@ -2096,21 +2119,21 @@ function createBot(token) {
|
|
|
2096
2119
|
|
|
2097
2120
|
bot.on('edited_message', async (ctx) => {
|
|
2098
2121
|
if (!isWellFormedMessage(ctx.editedMessage)) {
|
|
2099
|
-
|
|
2122
|
+
logEvent('malformed-update', {
|
|
2100
2123
|
bot: BOT_NAME,
|
|
2101
2124
|
update_id: ctx.update?.update_id,
|
|
2102
2125
|
reason: 'edited_message missing chat.id / message_id',
|
|
2103
|
-
})
|
|
2126
|
+
});
|
|
2104
2127
|
return;
|
|
2105
2128
|
}
|
|
2106
2129
|
const chatId = ctx.editedMessage.chat.id.toString();
|
|
2107
2130
|
if (!knownChat(chatId)) return;
|
|
2108
2131
|
recordInbound(ctx.editedMessage);
|
|
2109
|
-
|
|
2132
|
+
logEvent('message-edited', {
|
|
2110
2133
|
chat_id: chatId,
|
|
2111
2134
|
msg_id: ctx.editedMessage.message_id,
|
|
2112
2135
|
user_id: ctx.editedMessage.from?.id || null,
|
|
2113
|
-
})
|
|
2136
|
+
});
|
|
2114
2137
|
console.log(`[${BOT_NAME}] edited ${chatId}/${ctx.editedMessage.message_id}`);
|
|
2115
2138
|
});
|
|
2116
2139
|
|
|
@@ -2122,26 +2145,26 @@ function createBot(token) {
|
|
|
2122
2145
|
const rawNew = ctx.message?.migrate_to_chat_id;
|
|
2123
2146
|
const isValidId = (v) => (typeof v === 'number' && Number.isFinite(v)) || typeof v === 'bigint';
|
|
2124
2147
|
if (!isValidId(rawOld) || !isValidId(rawNew)) {
|
|
2125
|
-
|
|
2148
|
+
logEvent('malformed-update', {
|
|
2126
2149
|
bot: BOT_NAME,
|
|
2127
2150
|
update_id: ctx.update?.update_id,
|
|
2128
2151
|
reason: 'migrate_to_chat_id missing / non-numeric',
|
|
2129
|
-
})
|
|
2152
|
+
});
|
|
2130
2153
|
return;
|
|
2131
2154
|
}
|
|
2132
2155
|
const oldChatId = rawOld.toString();
|
|
2133
2156
|
const newChatId = rawNew.toString();
|
|
2134
2157
|
if (oldChatId === newChatId) {
|
|
2135
|
-
|
|
2158
|
+
logEvent('malformed-update', {
|
|
2136
2159
|
bot: BOT_NAME,
|
|
2137
2160
|
update_id: ctx.update?.update_id,
|
|
2138
2161
|
reason: 'migrate_to_chat_id equals current chat_id',
|
|
2139
|
-
})
|
|
2162
|
+
});
|
|
2140
2163
|
return;
|
|
2141
2164
|
}
|
|
2142
2165
|
console.log(`[${BOT_NAME}] chat migrated: ${oldChatId} → ${newChatId}`);
|
|
2143
2166
|
dbWrite(() => db.logChatMigration(oldChatId, newChatId), 'log chat-migration');
|
|
2144
|
-
|
|
2167
|
+
logEvent('chat-migrated', { old_chat_id: oldChatId, new_chat_id: newChatId });
|
|
2145
2168
|
if (config.chats[oldChatId] && !config.chats[newChatId]) {
|
|
2146
2169
|
config.chats[newChatId] = { ...config.chats[oldChatId] };
|
|
2147
2170
|
delete config.chats[oldChatId];
|
|
@@ -2158,12 +2181,12 @@ function createBot(token) {
|
|
|
2158
2181
|
const updateId = err.ctx?.update?.update_id;
|
|
2159
2182
|
const msgId = err.ctx?.update?.message?.message_id || err.ctx?.update?.edited_message?.message_id;
|
|
2160
2183
|
console.error(`[${BOT_NAME}] update ${updateId} msg ${msgId} error: ${err.message}`);
|
|
2161
|
-
|
|
2184
|
+
logEvent('update-error', {
|
|
2162
2185
|
bot: BOT_NAME,
|
|
2163
2186
|
update_id: updateId,
|
|
2164
2187
|
msg_id: msgId,
|
|
2165
2188
|
error: err.message?.slice(0, 300),
|
|
2166
|
-
})
|
|
2189
|
+
});
|
|
2167
2190
|
});
|
|
2168
2191
|
|
|
2169
2192
|
bot._setBotUsername = (u) => {
|
|
@@ -2269,12 +2292,12 @@ function startPollWatchdog(bot) {
|
|
|
2269
2292
|
if (age > POLL_STALL_MS) {
|
|
2270
2293
|
if (!stalled) {
|
|
2271
2294
|
console.error(`[${BOT_NAME}] poll-stalled: no tick in ${Math.round(age / 1000)}s`);
|
|
2272
|
-
|
|
2295
|
+
logEvent('poll-stalled', { bot: BOT_NAME, stall_ms: age });
|
|
2273
2296
|
stalled = true;
|
|
2274
2297
|
}
|
|
2275
2298
|
} else if (stalled) {
|
|
2276
2299
|
console.log(`[${BOT_NAME}] poll-recovered after stall`);
|
|
2277
|
-
|
|
2300
|
+
logEvent('poll-recovered', { bot: BOT_NAME });
|
|
2278
2301
|
stalled = false;
|
|
2279
2302
|
}
|
|
2280
2303
|
}, 30_000);
|
|
@@ -2356,7 +2379,7 @@ async function main() {
|
|
|
2356
2379
|
},
|
|
2357
2380
|
onClose: (sessionKey, code, entry) => {
|
|
2358
2381
|
console.log(`[${entry.label}] Process exited (code ${code})`);
|
|
2359
|
-
|
|
2382
|
+
logEvent('process-close', { chat_id: entry.chatId, session_key: sessionKey, code });
|
|
2360
2383
|
},
|
|
2361
2384
|
onStreamChunk: (sessionKey, partial, entry) => {
|
|
2362
2385
|
// Route to the head pending's per-turn streamer. In the 0.4.8
|
|
@@ -2431,22 +2454,22 @@ async function main() {
|
|
|
2431
2454
|
if (remaining > 0 && db) {
|
|
2432
2455
|
try {
|
|
2433
2456
|
const res = db.markReplayPending({ botName: BOT_NAME });
|
|
2434
|
-
|
|
2457
|
+
logEvent('shutdown-drain', {
|
|
2435
2458
|
bot: BOT_NAME,
|
|
2436
2459
|
in_flight: remaining,
|
|
2437
2460
|
replay_marked: res?.changes ?? 0,
|
|
2438
2461
|
elapsed_ms: drainElapsed,
|
|
2439
|
-
})
|
|
2462
|
+
});
|
|
2440
2463
|
console.log(`[shutdown] drained ${drainElapsed}ms, ${remaining} still in-flight, ${res?.changes ?? 0} rows marked replay-pending`);
|
|
2441
2464
|
} catch (err) {
|
|
2442
2465
|
console.error(`[shutdown] markReplayPending failed: ${err.message}`);
|
|
2443
2466
|
}
|
|
2444
2467
|
} else if (db) {
|
|
2445
|
-
|
|
2468
|
+
logEvent('shutdown-drain', {
|
|
2446
2469
|
bot: BOT_NAME,
|
|
2447
2470
|
in_flight: 0,
|
|
2448
2471
|
elapsed_ms: drainElapsed,
|
|
2449
|
-
})
|
|
2472
|
+
});
|
|
2450
2473
|
console.log(`[shutdown] clean drain in ${drainElapsed}ms`);
|
|
2451
2474
|
}
|
|
2452
2475
|
|
|
@@ -2491,13 +2514,19 @@ async function main() {
|
|
|
2491
2514
|
// Boot replay: re-dispatch any inbound turns that were interrupted by
|
|
2492
2515
|
// the previous polygram's shutdown or crash. These are rows marked
|
|
2493
2516
|
// 'dispatched', 'processing', or 'replay-pending' (set by the SIGTERM
|
|
2494
|
-
// handler) — all within the last
|
|
2495
|
-
//
|
|
2496
|
-
//
|
|
2517
|
+
// handler) — all within the last `replayWindowMs` (default 3 min) so
|
|
2518
|
+
// we don't resurrect ancient work. Override via
|
|
2519
|
+
// `config.bot.replayWindowMs` for ops tuning. Dedupe against
|
|
2520
|
+
// already-sent outbound replies in case the previous instance DID
|
|
2521
|
+
// answer before dying.
|
|
2497
2522
|
try {
|
|
2498
2523
|
const chatIds = Object.keys(config.chats);
|
|
2499
2524
|
if (chatIds.length > 0) {
|
|
2500
|
-
const
|
|
2525
|
+
const replayWindowMs = (() => {
|
|
2526
|
+
const v = Number(config.bot?.replayWindowMs);
|
|
2527
|
+
return (Number.isInteger(v) && v > 0) ? v : undefined; // undefined → use db.js default
|
|
2528
|
+
})();
|
|
2529
|
+
const candidates = db.getReplayCandidates({ chatIds, ...(replayWindowMs && { olderThanMs: replayWindowMs }) });
|
|
2501
2530
|
let replayed = 0;
|
|
2502
2531
|
let skipped = 0;
|
|
2503
2532
|
for (const row of candidates) {
|
|
@@ -2553,9 +2582,9 @@ async function main() {
|
|
|
2553
2582
|
}
|
|
2554
2583
|
if (candidates.length > 0) {
|
|
2555
2584
|
console.log(`[replay] ${replayed} turns re-dispatched, ${skipped} skipped (already replied or no chat config)`);
|
|
2556
|
-
|
|
2585
|
+
logEvent('replay-on-boot', {
|
|
2557
2586
|
bot: BOT_NAME, replayed, skipped, total: candidates.length,
|
|
2558
|
-
})
|
|
2587
|
+
});
|
|
2559
2588
|
}
|
|
2560
2589
|
}
|
|
2561
2590
|
} catch (err) {
|