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/config.example.json +5 -1
- package/lib/attachments.js +46 -2
- package/lib/claude-bin.js +8 -1
- package/lib/compaction-warn.js +59 -0
- package/lib/context-usage.js +93 -0
- package/lib/error/classify.js +12 -0
- package/lib/handlers/abort.js +38 -1
- package/lib/handlers/config-callback.js +8 -2
- package/lib/handlers/config-ui.js +23 -9
- package/lib/handlers/dispatcher.js +43 -0
- package/lib/handlers/download.js +101 -58
- package/lib/ipc/file-validator.js +8 -1
- package/lib/process/channels-tool-dispatcher.js +20 -2
- package/lib/process/cli-process.js +447 -73
- package/lib/process/factory.js +0 -5
- package/lib/process/hook-event-tail.js +7 -0
- package/lib/process/hook-settings.js +7 -0
- package/lib/process-manager.js +6 -0
- package/lib/sdk/callbacks.js +61 -7
- package/lib/telegram/album-reactions.js +50 -0
- package/lib/telegram/api.js +9 -0
- package/lib/telegram/input-file.js +76 -0
- package/lib/telegram/reactions.js +5 -0
- package/lib/tmux/log-tail.js +11 -1
- package/lib/tmux/startup-gate.js +65 -1
- package/package.json +1 -1
- package/polygram.js +64 -21
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
|
-
|
|
740
|
-
|
|
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
|
-
|
|
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
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
await
|
|
992
|
-
|
|
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: {
|
|
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]
|
|
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,
|