polygram 0.12.0-rc.2 → 0.12.0-rc.21

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/polygram.js CHANGED
@@ -28,7 +28,7 @@ const {
28
28
  migrateJsonToDb, getClaudeSessionId, resolveSessionForSpawn,
29
29
  } = require('./lib/db/sessions');
30
30
  const { buildPrompt } = require('./lib/prompt');
31
- const { filterAttachments } = require('./lib/attachments');
31
+ const { filterAttachments, resolveFileCaps, MAX_TOTAL_BYTES } = require('./lib/attachments');
32
32
  // 0.9.0: SDK ProcessManager is the only pm. CLI pm
33
33
  // (lib/process-manager.js) deleted in commit 6.
34
34
  // Both implementations expose the same public API (constructor +
@@ -51,7 +51,6 @@ const { extractAssistantText } = require('./lib/process/sdk-process');
51
51
  const { createChannelsToolDispatcher } = require('./lib/process/channels-tool-dispatcher');
52
52
  const { createTmuxRunner } = require('./lib/tmux/tmux-runner');
53
53
  const { sweepTmuxOrphans } = require('./lib/tmux/orphan-sweep');
54
- const { PollScheduler } = require('./lib/tmux/poll-scheduler');
55
54
  // rc.42: autosteer-buffer module deleted. Native SDK priority push
56
55
  // (pm.injectUserMessage) replaces the buffer + PostToolBatch detour.
57
56
  const { createAutosteeredRefs } = require('./lib/autosteered-refs');
@@ -98,6 +97,7 @@ const { startTyping } = require('./lib/telegram/typing');
98
97
  // consumer is lib/handlers/download.js.
99
98
  const { createReactionManager, classifyToolName } = require('./lib/telegram/reactions');
100
99
  const { createMediaGroupBuffer } = require('./lib/media-group-buffer');
100
+ const { applyReactionToMessages } = require('./lib/telegram/album-reactions');
101
101
  const { classify: classifyError, detectWedgedSessionError, isTransientHttpError } = require('./lib/error/classify');
102
102
  const { createAutoResumeTracker, isAutoResumable } = require('./lib/db/auto-resume');
103
103
  const { resolveReplayWindowMs } = require('./lib/db/replay-window');
@@ -462,6 +462,10 @@ function buildSpawnContext(sessionKey) {
462
462
  threadId: threadId || null,
463
463
  label: getSessionLabel(chatConfig, threadId),
464
464
  existingSessionId,
465
+ // File-send outbound cap inputs: localApi (bot-level) so CliProcess can
466
+ // resolve the per-chat/topic outbound cap (resolveFileCaps) the same way
467
+ // it resolves cwd/agent. Override itself lives in chatConfig/topic.
468
+ localApi: !!config.bot?.apiRoot,
465
469
  };
466
470
  }
467
471
 
@@ -736,8 +740,13 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
736
740
 
737
741
  if (botAllowsCommands && (text === '/model' || text === '/config' || text === '/effort')) {
738
742
  const show = text === '/effort' ? 'effort' : text === '/model' ? 'model' : 'all';
739
- const info = formatConfigInfoText(chatConfig, show, sessionKey);
740
- const reply_markup = buildConfigKeyboard(chatConfig, show);
743
+ // Resolve per-topic overrides so a topic's card shows its REAL
744
+ // agent/model/effort, not the chat-level default — Music topic (thread 3)
745
+ // showed "Agent: shumabit" instead of music-curation:music-curator
746
+ // (2026-06-03). getTopicConfig returns {} when there's no active topic.
747
+ const _cardTopicCfg = getTopicConfig(chatConfig, threadIdStr || null);
748
+ const info = formatConfigInfoText(chatConfig, show, sessionKey, _cardTopicCfg);
749
+ const reply_markup = buildConfigKeyboard(chatConfig, show, _cardTopicCfg);
741
750
  await sendReply(info, { params: { reply_markup } });
742
751
  return;
743
752
  }
@@ -755,7 +764,19 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
755
764
  const sessionCtx = !pm.has(sessionKey) ? await readSessionContext(sessionKey, chatConfig.cwd) : '';
756
765
 
757
766
  const rawAtts = extractAttachments(msg);
758
- const { accepted, rejected } = filterAttachments(rawAtts);
767
+ // Backend-derived inbound cap with per-topic/chat override. Cloud → 20MB;
768
+ // a local Bot API server (config.bot.apiRoot) → 2GB; override via
769
+ // chats[id].maxFileBytes or topics[t].maxFileBytes, clamped to the
770
+ // backend ceiling. Bytes-valued config; resolveFileCaps does the clamp.
771
+ const _inTopicCfg = getTopicConfig(chatConfig, threadIdStr || null);
772
+ const _fileCaps = resolveFileCaps({
773
+ localApi: !!config.bot?.apiRoot,
774
+ override: _inTopicCfg.maxFileBytes ?? chatConfig.maxFileBytes ?? null,
775
+ });
776
+ const { accepted, rejected } = filterAttachments(rawAtts, {
777
+ maxFileBytes: _fileCaps.inBytes,
778
+ maxTotalBytes: Math.max(_fileCaps.inBytes, MAX_TOTAL_BYTES),
779
+ });
759
780
  for (const { att, reason } of rejected) {
760
781
  console.log(`[${label}] attachment skipped: ${att.name} (${reason})`);
761
782
  logEvent('attachment-skipped', { chat_id: chatId, msg_id: msg.message_id, name: att.name, reason });
@@ -983,13 +1004,17 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
983
1004
  const availableEmojis = await getReactionAllowlist(bot, chatId);
984
1005
  const reactor = createReactionManager({
985
1006
  apply: async (emoji) => {
986
- const params = {
987
- chat_id: chatId,
988
- message_id: msg.message_id,
989
- reaction: emoji ? [{ type: 'emoji', emoji }] : [],
990
- };
991
- await tg(bot, 'setMessageReaction', params,
992
- { source: 'status-reaction', botName: BOT_NAME });
1007
+ // rc.16: mirror the reaction onto album siblings too, so a multi-file
1008
+ // send shows the same status emoji on EVERY item, not just the anchor.
1009
+ // For a normal single message, _albumSiblingMsgIds is undefined and this
1010
+ // is exactly the prior single setMessageReaction. Anchor is awaited
1011
+ // (failure surfaces to the reactor); siblings are best-effort.
1012
+ await applyReactionToMessages({
1013
+ tg, bot, chatId,
1014
+ msgIds: [msg.message_id, ...(msg._albumSiblingMsgIds || [])],
1015
+ emoji,
1016
+ botName: BOT_NAME,
1017
+ });
993
1018
  },
994
1019
  availableEmojis,
995
1020
  logError: (m) => console.error(`[${label}] ${m}`),
@@ -1673,9 +1698,30 @@ function shouldHandle(msg, chatConfig, botUsername) {
1673
1698
  }
1674
1699
 
1675
1700
  function createBot(token) {
1701
+ // Optional self-hosted Telegram Bot API server. When config.bot.apiRoot is
1702
+ // set (e.g. "http://localhost:8081" from a local `telegram-bot-api`
1703
+ // process), grammy routes all Bot API calls there instead of
1704
+ // api.telegram.org — which lifts file send/receive from cloud's 50 MB-out /
1705
+ // 20 MB-in to 2 GB both ways. Omit it (default) → cloud Telegram, unchanged.
1706
+ // The local server is a separate companion daemon; this is just the knob
1707
+ // that points polygram at it. See docs/0.12.0-file-send.md.
1708
+ const apiRoot = config.bot?.apiRoot;
1676
1709
  const bot = new Bot(token, {
1677
- client: { timeoutSeconds: 60 },
1710
+ client: {
1711
+ // rc.15: with the local Bot API server, getFile DOWNLOADS the file
1712
+ // synchronously (server fetches it from Telegram's DC, then responds) —
1713
+ // a large lossless WAV can take >60s, so the cloud-tuned 60s timeout
1714
+ // fired before the download finished (the file still landed on the
1715
+ // server's disk, but polygram's getFile call already errored). The
1716
+ // local server is localhost, so non-download calls stay fast; the
1717
+ // higher ceiling only matters for big getFile downloads.
1718
+ timeoutSeconds: apiRoot ? 180 : 60,
1719
+ ...(apiRoot ? { apiRoot } : {}),
1720
+ },
1678
1721
  });
1722
+ if (apiRoot) {
1723
+ console.log(`[polygram] using local Telegram Bot API server: ${apiRoot} (2GB file limit)`);
1724
+ }
1679
1725
  let botUsername = '';
1680
1726
  // Cached once @botUsername is known — was recompiling per inbound msg.
1681
1727
  let mentionRe = null;
@@ -1856,6 +1902,10 @@ function createBot(token) {
1856
1902
  }
1857
1903
 
1858
1904
  const synthetic = { ...primary, _mergedAttachments: merged };
1905
+ // rc.16: carry the album sibling msg_ids so the status reactor can mirror
1906
+ // its emoji onto every item (not just the anchor) — see the reactor
1907
+ // `apply` closure + lib/telegram/album-reactions.js.
1908
+ if (siblingMsgIds.length) synthetic._albumSiblingMsgIds = siblingMsgIds;
1859
1909
  // Carry the primary's text verbatim (dispatchRegularMessage re-cleans
1860
1910
  // the mention). Caption → text so downstream sees it uniformly.
1861
1911
  if (!synthetic.text && synthetic.caption) synthetic.text = synthetic.caption;
@@ -2244,19 +2294,13 @@ async function main() {
2244
2294
  const binCheck = verifyPinnedClaudeBin(CLAUDE_CLI_PINNED_VERSION);
2245
2295
  if (binCheck.ok) {
2246
2296
  console.log(
2247
- `[polygram] tmux backend pinned to claude CLI v${CLAUDE_CLI_PINNED_VERSION}: ${binCheck.path}`,
2297
+ `[polygram] CliProcess pinned to claude CLI v${CLAUDE_CLI_PINNED_VERSION}: ${binCheck.path}`,
2248
2298
  );
2249
2299
  pinnedClaudeBin = binCheck.path;
2250
2300
  } else {
2251
2301
  console.warn(`[polygram] WARNING: ${binCheck.reason}`);
2252
2302
  }
2253
2303
  }
2254
- // O1 optimization: shared poll-tick scheduler. N TmuxProcess
2255
- // instances share ONE setInterval instead of spawning N independent
2256
- // setTimeout chains. Idle when no chats are in flight (zero timers
2257
- // running). Configurable via config.bot.tmuxPollIntervalMs.
2258
- const tmuxPollIntervalMs = config.bot?.tmuxPollIntervalMs || 250;
2259
- const pollScheduler = new PollScheduler({ intervalMs: tmuxPollIntervalMs });
2260
2304
  // 0.11.0: channels backend wiring. Used when a chat opts in via
2261
2305
  // `pm: 'channels'` config. Falls back to SDK gracefully if the pinned
2262
2306
  // claude binary isn't present (see factory.js — channelsClaudeBin
@@ -2282,7 +2326,6 @@ async function main() {
2282
2326
  logger: console,
2283
2327
  tmuxRunner,
2284
2328
  botName: BOT_NAME,
2285
- pollScheduler,
2286
2329
  // channels backend
2287
2330
  toolDispatcher: channelsToolDispatcher,
2288
2331
  channelsClaudeBin,