polygram 0.7.1 → 0.7.2
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/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/lib/stream-reply.js +18 -0
- package/package.json +1 -1
- package/polygram.js +53 -10
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.2",
|
|
5
5
|
"description": "Telegram integration for Claude Code that preserves the OpenClaw per-chat session model. Migration target for OpenClaw users. Multi-bot, multi-chat, per-topic isolation; SQLite transcripts; inline-keyboard approvals. Bundles /polygram:status|logs|pair-code|approvals admin commands and a history skill.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"telegram",
|
package/README.md
CHANGED
|
@@ -364,7 +364,7 @@ foreign-chat clicks are rejected. Default-deny on IPC error.
|
|
|
364
364
|
## Development
|
|
365
365
|
|
|
366
366
|
```bash
|
|
367
|
-
npm test #
|
|
367
|
+
npm test # 643 tests, 158 suites, node:test, no external services
|
|
368
368
|
npm run coverage # native test coverage (Node 22+, no devDeps)
|
|
369
369
|
npm start -- --bot my-bot
|
|
370
370
|
npm run split-db -- --config config.json --dry-run
|
package/lib/stream-reply.js
CHANGED
|
@@ -57,6 +57,13 @@ function createStreamer({
|
|
|
57
57
|
let lastEditTs = 0;
|
|
58
58
|
let pendingEdit = null; // timer id
|
|
59
59
|
let flushPromise = null; // ongoing edit promise (for back-pressure)
|
|
60
|
+
// 0.7.2: msg_ids of bubbles that have been superseded by
|
|
61
|
+
// forceNewMessage(). The caller (polygram.js handleMessage at
|
|
62
|
+
// end-of-turn) reads getArchived() and issues deleteMessage on
|
|
63
|
+
// each — matches OpenClaw's archivedAnswerPreviews cleanup so
|
|
64
|
+
// the user sees only the final answer's bubble, not every
|
|
65
|
+
// "thinking out loud" intermediate from a tool-heavy turn.
|
|
66
|
+
const archived = [];
|
|
60
67
|
|
|
61
68
|
// LIVE-EDIT truncation only — used during streaming when latestText
|
|
62
69
|
// overshoots maxLen. The trailing "..." signals to the user that more
|
|
@@ -148,6 +155,11 @@ function createStreamer({
|
|
|
148
155
|
if (pendingEdit) { cancel(pendingEdit); pendingEdit = null; }
|
|
149
156
|
// Don't await flushPromise — the caller has decided to start a new
|
|
150
157
|
// message; whatever the old bubble shows is "done".
|
|
158
|
+
// 0.7.2: track the previous bubble's msgId for end-of-turn cleanup.
|
|
159
|
+
// Without this, every intermediate "thinking out loud" assistant
|
|
160
|
+
// message in a tool-heavy turn leaves a permanent bubble in the
|
|
161
|
+
// chat — the user wants only the final answer's bubble visible.
|
|
162
|
+
if (msgId != null) archived.push(msgId);
|
|
151
163
|
msgId = null;
|
|
152
164
|
currentText = '';
|
|
153
165
|
latestText = '';
|
|
@@ -239,6 +251,11 @@ function createStreamer({
|
|
|
239
251
|
}
|
|
240
252
|
}
|
|
241
253
|
|
|
254
|
+
// 0.7.2: snapshot of bubble msgIds that forceNewMessage() superseded.
|
|
255
|
+
// Returns a copy so callers can't mutate internal state. polygram.js
|
|
256
|
+
// reads this at end-of-turn and issues deleteMessage on each.
|
|
257
|
+
function getArchived() { return archived.slice(); }
|
|
258
|
+
|
|
242
259
|
return {
|
|
243
260
|
onChunk,
|
|
244
261
|
finalize,
|
|
@@ -246,6 +263,7 @@ function createStreamer({
|
|
|
246
263
|
forceNewMessage,
|
|
247
264
|
discard,
|
|
248
265
|
archive,
|
|
266
|
+
getArchived,
|
|
249
267
|
// Introspection for tests:
|
|
250
268
|
get state() { return state; },
|
|
251
269
|
get msgId() { return msgId; },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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
|
@@ -1664,21 +1664,34 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1664
1664
|
...(linkPreview === false ? { linkPreview: false } : {}),
|
|
1665
1665
|
};
|
|
1666
1666
|
|
|
1667
|
+
// 0.7.2: only the FIRST bubble in a turn quotes the user's message
|
|
1668
|
+
// via reply_parameters. When a tool-heavy turn produces multiple
|
|
1669
|
+
// assistant messages (each spawning its own bubble via
|
|
1670
|
+
// forceNewMessage), subsequent bubbles shouldn't re-quote the user
|
|
1671
|
+
// — the chat would show N copies of the same quoted message stacked
|
|
1672
|
+
// vertically. After the first send, the flag flips and subsequent
|
|
1673
|
+
// initial-sends omit reply_parameters.
|
|
1674
|
+
let firstBubbleSent = false;
|
|
1667
1675
|
// Streaming is unconditional as of 0.4.0 — matches OpenClaw's model and
|
|
1668
1676
|
// eliminates the "stuck at 15min typing" complaint from the non-streaming
|
|
1669
1677
|
// code path. For short responses the streamer stays idle and we fall
|
|
1670
1678
|
// through to the normal send path via finalize() returning streamed=false.
|
|
1671
1679
|
const streamer = createStreamer({
|
|
1672
|
-
send: async (text) =>
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1680
|
+
send: async (text) => {
|
|
1681
|
+
const params = {
|
|
1682
|
+
chat_id: chatId, text,
|
|
1683
|
+
...(threadId && { message_thread_id: threadId }),
|
|
1684
|
+
};
|
|
1685
|
+
if (!firstBubbleSent) {
|
|
1686
|
+
// allow_sending_without_reply: long-running turns give the user
|
|
1687
|
+
// plenty of time to delete their original message. Without this
|
|
1688
|
+
// flag, Telegram rejects the reply with MESSAGE_NOT_FOUND and the
|
|
1689
|
+
// whole streamed answer is lost.
|
|
1690
|
+
params.reply_parameters = { message_id: msg.message_id, allow_sending_without_reply: true };
|
|
1691
|
+
firstBubbleSent = true;
|
|
1692
|
+
}
|
|
1693
|
+
return tg(bot, 'sendMessage', params, outMetaBase);
|
|
1694
|
+
},
|
|
1682
1695
|
edit: async (messageId, text) => {
|
|
1683
1696
|
try {
|
|
1684
1697
|
// Route edits through tg() so applyFormatting runs (MarkdownV2
|
|
@@ -1725,6 +1738,34 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1725
1738
|
});
|
|
1726
1739
|
// streamer is registered with this turn via pm.send's context (below)
|
|
1727
1740
|
|
|
1741
|
+
// 0.7.2: clean up bubbles superseded by forceNewMessage() — the
|
|
1742
|
+
// intermediate "thinking out loud" assistant messages that fired in
|
|
1743
|
+
// a tool-heavy turn. Without this, every tool-result cycle leaves a
|
|
1744
|
+
// permanent bubble in the chat (see the screenshot from the post-
|
|
1745
|
+
// 0.7.1 deploy where six bubbles appeared for one logical turn).
|
|
1746
|
+
// Matches OpenClaw's archivedAnswerPreviews end-of-turn cleanup.
|
|
1747
|
+
// Call AFTER finalize/discard decisions so we never delete the
|
|
1748
|
+
// bubble that's the final reply.
|
|
1749
|
+
async function cleanupArchivedBubbles() {
|
|
1750
|
+
const archived = streamer.getArchived?.() || [];
|
|
1751
|
+
if (archived.length === 0) return;
|
|
1752
|
+
for (const messageId of archived) {
|
|
1753
|
+
try {
|
|
1754
|
+
await tg(bot, 'deleteMessage', {
|
|
1755
|
+
chat_id: chatId, message_id: messageId,
|
|
1756
|
+
}, { source: 'bot-reply-archived-cleanup', botName: BOT_NAME });
|
|
1757
|
+
} catch (err) {
|
|
1758
|
+
// Non-fatal — message may be >48h old or already gone.
|
|
1759
|
+
// Operator-visible only via the events table.
|
|
1760
|
+
console.error(`[${label}] archived-cleanup ${messageId}: ${err.message}`);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
logEvent('telegram-archived-cleanup', {
|
|
1764
|
+
chat_id: chatId, msg_id: msg.message_id, count: archived.length,
|
|
1765
|
+
bot: BOT_NAME,
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1728
1769
|
// Status reactions on the user's message: 👀 queued → 🤔 thinking →
|
|
1729
1770
|
// 👨💻 coding / ⚡ web / 🔥 tool → 👍 done / 🤯 error. Silent (no
|
|
1730
1771
|
// notifications), updates in place, one emoji per message. Uses
|
|
@@ -1853,6 +1894,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1853
1894
|
if (fin.finalEditOk) {
|
|
1854
1895
|
// Preview was successfully edited to the final text.
|
|
1855
1896
|
// No follow-up messages needed.
|
|
1897
|
+
await cleanupArchivedBubbles();
|
|
1856
1898
|
console.log(`[${label}] ${elapsed}s | ${result.text.length} chars | streamed | ${chatConfig.model}/${chatConfig.effort} | $${result.cost?.toFixed(4) || '?'}`);
|
|
1857
1899
|
markReplied();
|
|
1858
1900
|
return;
|
|
@@ -1897,6 +1939,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1897
1939
|
console.error(`[${label}] partial-delivery warning failed: ${warnErr.message}`);
|
|
1898
1940
|
}
|
|
1899
1941
|
}
|
|
1942
|
+
await cleanupArchivedBubbles();
|
|
1900
1943
|
console.log(`[${label}] ${elapsed}s | ${result.text.length} chars | streamed-redeliver(${reason}, ${chunks.length} chunks${r.failed.length ? `, ${r.failed.length} failed` : ''}) | ${chatConfig.model}/${chatConfig.effort} | $${result.cost?.toFixed(4) || '?'}`);
|
|
1901
1944
|
markReplied();
|
|
1902
1945
|
return;
|