polygram 0.9.0-rc.4 → 0.9.0-rc.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "polygram",
4
- "version": "0.9.0-rc.4",
4
+ "version": "0.9.0-rc.6",
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 plus history (transcript queries) and polygram-send (out-of-turn IPC sends with file-upload validation) skills.",
6
6
  "keywords": [
7
7
  "telegram",
package/lib/db.js CHANGED
@@ -521,6 +521,20 @@ function wrap(db) {
521
521
  `).run(status, chat_id, msg_id);
522
522
  },
523
523
 
524
+ // 0.9.0: True when a specific inbound msg is still being processed
525
+ // by the SDK turn loop (handler_status in dispatched/processing).
526
+ // Used by the edit-correction injector — only inject a typo-fix
527
+ // note when the SDK actually still has the turn in flight.
528
+ isInboundLive({ chat_id, msg_id }) {
529
+ const row = db.prepare(`
530
+ SELECT 1 FROM messages
531
+ WHERE chat_id = ? AND msg_id = ? AND direction = 'in'
532
+ AND handler_status IN ('dispatched', 'processing')
533
+ LIMIT 1
534
+ `).get(chat_id, msg_id);
535
+ return !!row;
536
+ },
537
+
524
538
  // Find inbound messages that were being processed when polygram stopped.
525
539
  // Scoped by bot_name via the chat_id → config mapping, so each bot only
526
540
  // replays its own turns on boot. Scoped by olderThanMs (default 3 min)
@@ -41,7 +41,13 @@ function createDispatcher({
41
41
  autoResumeTracker, // lib/db/auto-resume.js instance
42
42
  chunkMarkdownText, // lib/telegram/chunk.js
43
43
  deliverReplies, // lib/telegram/deliver.js
44
- TG_MAX_LEN = 4096,
44
+ // Raw-markdown size budget for chunkMarkdownText. Set BELOW Telegram's
45
+ // 4096 hard limit to leave headroom for HTML inflation (toTelegramHtml
46
+ // adds <b>/<i>/<code> tags + entity escapes; ~10-15% in practice).
47
+ // Polygram passes TG_CHUNK_BUDGET (default 3500). Test default keeps
48
+ // the historic 4096 for back-compat in synthetic test runs that pass
49
+ // pre-formatted text.
50
+ chunkBudget = 4096,
45
51
  // State accessors (need late binding because polygram.js mutates):
46
52
  getIsShuttingDown, // () → boolean
47
53
  logger = console,
@@ -119,7 +125,7 @@ function createDispatcher({
119
125
 
120
126
  // 4. Send the continuation reply as regular Telegram messages,
121
127
  // threaded under the original user message.
122
- const chunks = chunkMarkdownText(result.text, TG_MAX_LEN);
128
+ const chunks = chunkMarkdownText(result.text, chunkBudget);
123
129
  await deliverReplies({
124
130
  bot,
125
131
  send: (b, method, params, m) => tg(b, method, params, m),
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Edit-correction injector.
5
+ *
6
+ * When the user edits a Telegram message that's still being processed
7
+ * by the SDK turn loop, inject a correction note into the active turn
8
+ * via the same hook channel autosteer uses (pm.injectUserMessage,
9
+ * priority: 'next'). Lets users fix typos mid-turn without /stop +
10
+ * resend.
11
+ *
12
+ * Skipped when the turn already finished — at that point the
13
+ * conversation has moved on and re-opening it would confuse Claude
14
+ * more than help. Also skipped when the SDK session has been
15
+ * LRU-evicted (no live target to inject into).
16
+ *
17
+ * Architectural note: there's no polygram-side "buffer" to replace.
18
+ * pm.send and pm.injectUserMessage push the message text directly into
19
+ * the SDK's inputController AsyncIterable; once pushed, polygram has
20
+ * no way to retract it. So edit handling is always "inject correction
21
+ * note as additional context" — Claude reconciles.
22
+ */
23
+
24
+ function createEditCorrectionInjector({
25
+ pm,
26
+ db,
27
+ getSessionKey,
28
+ config,
29
+ logEvent,
30
+ logger = console,
31
+ } = {}) {
32
+
33
+ return function maybeInjectEditCorrection(editedMsg) {
34
+ if (!editedMsg?.chat) return false;
35
+ const chatId = editedMsg.chat.id.toString();
36
+ const chatConfig = config.chats[chatId];
37
+ if (!chatConfig) return false;
38
+
39
+ // Per-chat / bot-level opt-out. Default on.
40
+ const optOut = chatConfig.editCorrection != null
41
+ ? chatConfig.editCorrection === false
42
+ : config.bot?.editCorrection === false;
43
+ if (optOut) return false;
44
+
45
+ const threadIdStr = editedMsg.message_thread_id?.toString() || null;
46
+ const sessionKey = getSessionKey(chatId, threadIdStr, chatConfig);
47
+
48
+ // Three skip gates — all must be true for an injection:
49
+ // 1. SDK session exists (not LRU-evicted)
50
+ // 2. session has a turn in flight
51
+ // 3. the edited msg is still in the dispatched/processing pipeline
52
+ if (!pm.has(sessionKey)) return false;
53
+ if (!pm.get(sessionKey)?.inFlight) return false;
54
+ if (!db.isInboundLive({ chat_id: chatId, msg_id: editedMsg.message_id })) return false;
55
+
56
+ const newText = editedMsg.text || editedMsg.caption || '';
57
+ if (!newText) return false;
58
+
59
+ const ok = pm.injectUserMessage(sessionKey, {
60
+ content: `[edit] I corrected my previous message — it now reads: ${newText}`,
61
+ priority: 'next',
62
+ });
63
+ if (!ok) {
64
+ logger.error?.(`[${chatConfig.name || chatId}] edit-correction inject failed`);
65
+ return false;
66
+ }
67
+ logEvent('message-edit-injected', {
68
+ chat_id: chatId,
69
+ msg_id: editedMsg.message_id,
70
+ session_key: sessionKey,
71
+ text_len: newText.length,
72
+ });
73
+ return true;
74
+ };
75
+ }
76
+
77
+ module.exports = { createEditCorrectionInjector };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.9.0-rc.4",
3
+ "version": "0.9.0-rc.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
@@ -44,6 +44,7 @@ const { createDownloadAttachments } = require('./lib/handlers/download');
44
44
  const { createHandleConfigCallback } = require('./lib/handlers/config-callback');
45
45
  const { createHandleAbort } = require('./lib/handlers/abort');
46
46
  const { createAutosteerHandlers } = require('./lib/handlers/autosteer');
47
+ const { createEditCorrectionInjector } = require('./lib/handlers/edit-correction');
47
48
  const { createSlashCommands } = require('./lib/handlers/slash-commands');
48
49
  const { createApprovals } = require('./lib/handlers/approvals');
49
50
  const { canonicalizeToolInput } = require('./lib/canonical-json');
@@ -108,6 +109,15 @@ const CLAUDE_BIN = process.env.POLYGRAM_CLAUDE_BIN
108
109
  || path.join(process.env.HOME || '', '.npm-global/bin/claude');
109
110
  const CHILD_HOME = process.env.POLYGRAM_CHILD_HOME || process.env.HOME || '';
110
111
  const TG_MAX_LEN = 4096;
112
+ // 0.9.0-rc.6: chunker budget is intentionally lower than TG_MAX_LEN to
113
+ // leave HTML headroom. toTelegramHtml converts markdown to HTML for
114
+ // parse_mode=HTML — that conversion adds <b>/<i>/<code>/<a> tags and
115
+ // entity-escapes &/</> chars, inflating length by ~10-15% for realistic
116
+ // markdown. 2026-05-11 incident proved a 4044-char chunk inflated to
117
+ // 4506 HTML chars and Telegram rejected. 3500 raw → max ~4030 HTML on
118
+ // observed inputs, with headroom for adversarial code-heavy text.
119
+ // Override via POLYGRAM_CHUNK_BUDGET if your traffic profile differs.
120
+ const TG_CHUNK_BUDGET = Number.parseInt(process.env.POLYGRAM_CHUNK_BUDGET, 10) || 3500;
111
121
  const DEFAULT_MAX_WARM_PROCS = 10;
112
122
 
113
123
  let stickerMap = {}; // name → file_id
@@ -525,6 +535,7 @@ let handleConfigCallback = null;
525
535
  let handleAbortIfRequested = null;
526
536
  let autosteer = null;
527
537
  let dispatchSlashCommand = null;
538
+ let maybeInjectEditCorrection = null;
528
539
 
529
540
  // rc.20: approvalCardText + safeParse moved to lib/approvals/ui.js.
530
541
  // 0.9.0 commit 29: makeCanUseTool / handleApprovalCallback /
@@ -1251,7 +1262,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
1251
1262
  // send the body as proper chunks.
1252
1263
  try { await streamer.discard(); }
1253
1264
  catch (err) { console.error(`[${label}] discard failed: ${err.message}`); }
1254
- const chunks = chunkMarkdownText(parsed.text, TG_MAX_LEN);
1265
+ const chunks = chunkMarkdownText(parsed.text, TG_CHUNK_BUDGET);
1255
1266
  const r = await deliverReplies({
1256
1267
  bot,
1257
1268
  send: (b, method, params, m) => tg(b, method, params, m),
@@ -1317,7 +1328,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
1317
1328
  // 0.7.0: use markdown-aware chunker + deliverReplies primitive.
1318
1329
  // The old chunkText was newline/byte-only; chunkMarkdownText also
1319
1330
  // respects code-fence boundaries (closes + reopens across chunks).
1320
- const chunks = chunkMarkdownText(parsed.text, TG_MAX_LEN);
1331
+ const chunks = chunkMarkdownText(parsed.text, TG_CHUNK_BUDGET);
1321
1332
  await deliverReplies({
1322
1333
  bot,
1323
1334
  send: (b, method, params, m) => tg(b, method, params, m),
@@ -1702,6 +1713,18 @@ function createBot(token) {
1702
1713
  user_id: ctx.editedMessage.from?.id || null,
1703
1714
  });
1704
1715
  console.log(`[${BOT_NAME}] edited ${chatId}/${ctx.editedMessage.message_id}`);
1716
+
1717
+ // 0.9.0: typo-correction injection. If the SDK still has this turn
1718
+ // in flight (handler_status in dispatched/processing AND
1719
+ // pm.get(sk).inFlight), inject a `[edit] corrected: <NEW>` note
1720
+ // via the same channel autosteer uses. Lets users fix typos
1721
+ // mid-turn without /stop + resend. No-op when the turn already
1722
+ // completed.
1723
+ try {
1724
+ maybeInjectEditCorrection?.(ctx.editedMessage);
1725
+ } catch (err) {
1726
+ console.error(`[${BOT_NAME}] edit-correction injector error: ${err.message}`);
1727
+ }
1705
1728
  });
1706
1729
 
1707
1730
  bot.on('message:migrate_to_chat_id', async (ctx) => {
@@ -1955,6 +1978,9 @@ async function main() {
1955
1978
  autosteer = createAutosteerHandlers({
1956
1979
  config, pm, autosteeredRefs, logEvent,
1957
1980
  });
1981
+ maybeInjectEditCorrection = createEditCorrectionInjector({
1982
+ pm, db, getSessionKey, config, logEvent, logger: console,
1983
+ });
1958
1984
  recordInbound = createRecordInbound({
1959
1985
  db, dbWrite, config, botName: BOT_NAME, extractAttachments,
1960
1986
  });
@@ -1973,7 +1999,7 @@ async function main() {
1973
1999
  classifyError, isAutoResumable,
1974
2000
  abortGrace, autoResumeTracker,
1975
2001
  chunkMarkdownText, deliverReplies,
1976
- TG_MAX_LEN,
2002
+ chunkBudget: TG_CHUNK_BUDGET,
1977
2003
  getIsShuttingDown: () => isShuttingDown,
1978
2004
  logger: console,
1979
2005
  }));