polygram 0.12.0-rc.7 → 0.12.0-rc.8
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 +3 -1
- package/lib/attachments.js +40 -1
- package/lib/process/channels-tool-dispatcher.js +16 -1
- package/lib/process/cli-process.js +22 -3
- package/package.json +1 -1
- package/polygram.js +18 -2
package/config.example.json
CHANGED
|
@@ -71,7 +71,9 @@
|
|
|
71
71
|
"model": "opus",
|
|
72
72
|
"effort": "medium",
|
|
73
73
|
"cwd": "/Users/you/admin-agent",
|
|
74
|
-
"timeout": 600
|
|
74
|
+
"timeout": 600,
|
|
75
|
+
"_comment_maxFileBytes": "Optional per-chat file-size cap in BYTES for send/receive, also settable per-topic (topic wins). Default follows the backend: cloud Telegram 50MB send / 20MB receive; with bot.apiRoot (local Bot API server) 2GB both ways. An override can only LOWER the cap below the backend ceiling — Telegram rejects anything larger regardless. Example: 104857600 = 100MB (only effective when apiRoot is set).",
|
|
76
|
+
"maxFileBytes": 104857600
|
|
75
77
|
},
|
|
76
78
|
|
|
77
79
|
"-1000000000001": {
|
package/lib/attachments.js
CHANGED
|
@@ -26,9 +26,44 @@
|
|
|
26
26
|
// bot file DOWNLOADS (getFile) at 20 MB, so 20 MB is the real ceiling on
|
|
27
27
|
// cloud — raised from 10 MB so users can send larger tracks/docs. With a
|
|
28
28
|
// self-hosted Bot API server (config.bot.apiRoot) the Telegram limit rises
|
|
29
|
-
// to 2 GB;
|
|
29
|
+
// to 2 GB; resolveFileCaps() raises the default accordingly.
|
|
30
30
|
const MAX_FILE_BYTES = 20 * 1024 * 1024;
|
|
31
31
|
const MAX_TOTAL_BYTES = 50 * 1024 * 1024;
|
|
32
|
+
|
|
33
|
+
// ─── Backend-derived file-size caps (cloud vs local Bot API server) ──
|
|
34
|
+
//
|
|
35
|
+
// These are the HARD ceilings Telegram itself enforces — a per-chat
|
|
36
|
+
// override can lower them but never exceed them (Telegram rejects beyond
|
|
37
|
+
// regardless). NOT "adaptive": there is no intermediate tier. Cloud is a
|
|
38
|
+
// flat 20 in / 50 out; a local `telegram-bot-api --local` server is a flat
|
|
39
|
+
// 2 GB both ways.
|
|
40
|
+
const CLOUD_MAX_IN_BYTES = 20 * 1024 * 1024; // getFile download limit
|
|
41
|
+
const CLOUD_MAX_OUT_BYTES = 50 * 1024 * 1024; // sendDocument upload limit
|
|
42
|
+
const LOCAL_MAX_BYTES = 2000 * 1024 * 1024; // --local server, both ways
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the effective per-file caps for a chat/topic.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} opts
|
|
48
|
+
* @param {boolean} opts.localApi — true when config.bot.apiRoot is set
|
|
49
|
+
* (a local Bot API server is in use → 2 GB ceiling).
|
|
50
|
+
* @param {...number} opts.override — per-chat/topic maxFileBytes (bytes).
|
|
51
|
+
* Resolved by the caller from topic → chat → undefined; clamped to the
|
|
52
|
+
* backend ceiling.
|
|
53
|
+
* @returns {{ inBytes:number, outBytes:number, ceiling:number, localApi:boolean }}
|
|
54
|
+
*/
|
|
55
|
+
function resolveFileCaps({ localApi = false, override = null } = {}) {
|
|
56
|
+
const ceiling = localApi ? LOCAL_MAX_BYTES : null;
|
|
57
|
+
const defIn = localApi ? LOCAL_MAX_BYTES : CLOUD_MAX_IN_BYTES;
|
|
58
|
+
const defOut = localApi ? LOCAL_MAX_BYTES : CLOUD_MAX_OUT_BYTES;
|
|
59
|
+
// A numeric override sets BOTH directions to the same value, clamped to
|
|
60
|
+
// the backend hard ceiling (cloud uses the per-direction default as the
|
|
61
|
+
// clamp so an override can't push past Telegram's own limit).
|
|
62
|
+
const ovr = (typeof override === 'number' && override > 0) ? override : null;
|
|
63
|
+
const inBytes = ovr ? (localApi ? Math.min(ovr, ceiling) : Math.min(ovr, CLOUD_MAX_IN_BYTES)) : defIn;
|
|
64
|
+
const outBytes = ovr ? (localApi ? Math.min(ovr, ceiling) : Math.min(ovr, CLOUD_MAX_OUT_BYTES)) : defOut;
|
|
65
|
+
return { inBytes, outBytes, ceiling: ceiling ?? CLOUD_MAX_OUT_BYTES, localApi };
|
|
66
|
+
}
|
|
32
67
|
const MIME_ALLOW = [
|
|
33
68
|
/^image\//, /^audio\//, /^video\//,
|
|
34
69
|
/^application\/pdf$/, /^text\/plain$/,
|
|
@@ -114,8 +149,12 @@ function filterAttachments(attachments, opts = {}) {
|
|
|
114
149
|
|
|
115
150
|
module.exports = {
|
|
116
151
|
filterAttachments,
|
|
152
|
+
resolveFileCaps,
|
|
117
153
|
MAX_FILE_BYTES,
|
|
118
154
|
MAX_TOTAL_BYTES,
|
|
155
|
+
CLOUD_MAX_IN_BYTES,
|
|
156
|
+
CLOUD_MAX_OUT_BYTES,
|
|
157
|
+
LOCAL_MAX_BYTES,
|
|
119
158
|
MIME_ALLOW,
|
|
120
159
|
EXTENSION_ALLOW,
|
|
121
160
|
FALLBACK_MIMES,
|
|
@@ -125,7 +125,7 @@ function createChannelsToolDispatcher({
|
|
|
125
125
|
|| require('../telegram/process-agent-reply').processAndDeliverAgentText;
|
|
126
126
|
|
|
127
127
|
return async function channelsToolDispatcher(call) {
|
|
128
|
-
const { sessionKey, chatId, threadId, toolName, text, files, sourceMsgId } = call;
|
|
128
|
+
const { sessionKey, chatId, threadId, toolName, text, files, sourceMsgId, maxOutboundFileBytes } = call;
|
|
129
129
|
|
|
130
130
|
if (toolName !== 'reply') {
|
|
131
131
|
// 0.11.0 Phase 1 ships `reply` only — react and edit_message are
|
|
@@ -196,6 +196,21 @@ function createChannelsToolDispatcher({
|
|
|
196
196
|
failedAttachments.push({ path: filePath, error: check.error });
|
|
197
197
|
continue;
|
|
198
198
|
}
|
|
199
|
+
// Backend/chat-derived upload cap. Reject oversize BEFORE upload with
|
|
200
|
+
// a clear error (vs Telegram's cryptic 413/"file is too big") so
|
|
201
|
+
// claude can convert/compress and retry. maxOutboundFileBytes is
|
|
202
|
+
// undefined for non-channels callers → no cap (Telegram still gates).
|
|
203
|
+
if (typeof maxOutboundFileBytes === 'number' && maxOutboundFileBytes > 0) {
|
|
204
|
+
let size = 0;
|
|
205
|
+
try { size = fs.statSync(check.resolved).size; } catch {}
|
|
206
|
+
if (size > maxOutboundFileBytes) {
|
|
207
|
+
const mb = (n) => (n / (1024 * 1024)).toFixed(1);
|
|
208
|
+
const err = `file too large to send: ${mb(size)}MB > ${mb(maxOutboundFileBytes)}MB limit`;
|
|
209
|
+
logger.warn?.(`[channels-tool-dispatcher] ${err} (${check.resolved})`);
|
|
210
|
+
failedAttachments.push({ path: filePath, error: err });
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
199
214
|
try {
|
|
200
215
|
const ext = path.extname(check.resolved).toLowerCase();
|
|
201
216
|
const isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].includes(ext);
|
|
@@ -52,6 +52,7 @@ const { createHookTail } = require('./hook-event-tail');
|
|
|
52
52
|
// create exactly matches the realpath the validator accepts (no /tmp vs
|
|
53
53
|
// /private/tmp drift — one of the original Music-topic failures).
|
|
54
54
|
const { DEFAULT_ATTACHMENT_BASE } = require('./channels-tool-dispatcher');
|
|
55
|
+
const { resolveFileCaps } = require('../attachments');
|
|
55
56
|
const { runStartupGate } = require('../tmux/startup-gate');
|
|
56
57
|
const { POLYGRAM_DISPLAY_HINT } = require('../telegram/display-hint');
|
|
57
58
|
|
|
@@ -255,6 +256,10 @@ class CliProcess extends Process {
|
|
|
255
256
|
// pending turn(s): turn_id → { resolve, reject, replies: [], quietTimer, hardTimer, startedAt }
|
|
256
257
|
this.pendingTurns = new Map();
|
|
257
258
|
|
|
259
|
+
// File-send outbound cap (bot → user). Safe cloud default; overwritten in
|
|
260
|
+
// _spawnTmuxClaude with the backend/chat-resolved value before any turn.
|
|
261
|
+
this.maxOutboundFileBytes = resolveFileCaps({ localApi: false }).outBytes;
|
|
262
|
+
|
|
258
263
|
// P1 security (review #8): track resolved permission request_ids so a
|
|
259
264
|
// double-fire of respond() can't write a second perm_verdict for the same
|
|
260
265
|
// request. TmuxProcess gates on _pendingApprovalId; this is the channels
|
|
@@ -493,6 +498,18 @@ class CliProcess extends Process {
|
|
|
493
498
|
const effort = topicConfig?.effort || opts.chatConfig?.effort || opts.effort;
|
|
494
499
|
const resolvedCwd = topicConfig?.cwd || opts.chatConfig?.cwd || opts.cwd;
|
|
495
500
|
|
|
501
|
+
// File-send outbound cap (bot → user). Backend-derived (cloud 50MB vs
|
|
502
|
+
// local Bot API server 2GB via opts.localApi) with per-topic/chat
|
|
503
|
+
// maxFileBytes override, clamped to the backend ceiling. Stored for the
|
|
504
|
+
// dispatcher (live size-check) and the system prompt (so claude states
|
|
505
|
+
// the right limit). Resolved here so it follows the same topic→chat
|
|
506
|
+
// precedence as cwd/agent above.
|
|
507
|
+
const _capOverride = topicConfig?.maxFileBytes ?? opts.chatConfig?.maxFileBytes ?? null;
|
|
508
|
+
this.maxOutboundFileBytes = resolveFileCaps({
|
|
509
|
+
localApi: !!opts.localApi,
|
|
510
|
+
override: _capOverride,
|
|
511
|
+
}).outBytes;
|
|
512
|
+
|
|
496
513
|
// Parity audit P8 + rc.8 fs-guard (2026-05-26 shumorobot Music topic):
|
|
497
514
|
// `--session-id <id>` creates a NEW claude session with that id;
|
|
498
515
|
// `--resume <id>` resumes the EXISTING conversation. Lazy-respawn after
|
|
@@ -637,9 +654,10 @@ class CliProcess extends Process {
|
|
|
637
654
|
'path in `files`. Paths outside the workspace are rejected for safety.',
|
|
638
655
|
]),
|
|
639
656
|
'',
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
657
|
+
`Max file size for sending: ${Math.round(this.maxOutboundFileBytes / (1024 * 1024))} MB. ` +
|
|
658
|
+
'For larger lossless audio, convert to FLAC/MP3 under the limit first, ' +
|
|
659
|
+
'or tell the user it exceeds the limit. Images go as photos; everything ' +
|
|
660
|
+
'else as documents.',
|
|
643
661
|
].join('\n'));
|
|
644
662
|
|
|
645
663
|
// Parity audit P6: honor isolateUserConfig — mirrors tmux pattern at
|
|
@@ -984,6 +1002,7 @@ class CliProcess extends Process {
|
|
|
984
1002
|
text: args.text,
|
|
985
1003
|
files: args.files,
|
|
986
1004
|
sessionCwd: this.sessionCwd, // P0 #2: dispatcher uses this to allowlist file roots
|
|
1005
|
+
maxOutboundFileBytes: this.maxOutboundFileBytes, // backend/chat-derived upload cap
|
|
987
1006
|
});
|
|
988
1007
|
} catch (err) {
|
|
989
1008
|
this._writeToBridge({ kind: 'tool_ack', tool_call_id: msg.tool_call_id, ok: false, error: err.message });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.12.0-rc.
|
|
3
|
+
"version": "0.12.0-rc.8",
|
|
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
|
@@ -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 +
|
|
@@ -461,6 +461,10 @@ function buildSpawnContext(sessionKey) {
|
|
|
461
461
|
threadId: threadId || null,
|
|
462
462
|
label: getSessionLabel(chatConfig, threadId),
|
|
463
463
|
existingSessionId,
|
|
464
|
+
// File-send outbound cap inputs: localApi (bot-level) so CliProcess can
|
|
465
|
+
// resolve the per-chat/topic outbound cap (resolveFileCaps) the same way
|
|
466
|
+
// it resolves cwd/agent. Override itself lives in chatConfig/topic.
|
|
467
|
+
localApi: !!config.bot?.apiRoot,
|
|
464
468
|
};
|
|
465
469
|
}
|
|
466
470
|
|
|
@@ -754,7 +758,19 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
754
758
|
const sessionCtx = !pm.has(sessionKey) ? await readSessionContext(sessionKey, chatConfig.cwd) : '';
|
|
755
759
|
|
|
756
760
|
const rawAtts = extractAttachments(msg);
|
|
757
|
-
|
|
761
|
+
// Backend-derived inbound cap with per-topic/chat override. Cloud → 20MB;
|
|
762
|
+
// a local Bot API server (config.bot.apiRoot) → 2GB; override via
|
|
763
|
+
// chats[id].maxFileBytes or topics[t].maxFileBytes, clamped to the
|
|
764
|
+
// backend ceiling. Bytes-valued config; resolveFileCaps does the clamp.
|
|
765
|
+
const _inTopicCfg = getTopicConfig(chatConfig, threadIdStr || null);
|
|
766
|
+
const _fileCaps = resolveFileCaps({
|
|
767
|
+
localApi: !!config.bot?.apiRoot,
|
|
768
|
+
override: _inTopicCfg.maxFileBytes ?? chatConfig.maxFileBytes ?? null,
|
|
769
|
+
});
|
|
770
|
+
const { accepted, rejected } = filterAttachments(rawAtts, {
|
|
771
|
+
maxFileBytes: _fileCaps.inBytes,
|
|
772
|
+
maxTotalBytes: Math.max(_fileCaps.inBytes, MAX_TOTAL_BYTES),
|
|
773
|
+
});
|
|
758
774
|
for (const { att, reason } of rejected) {
|
|
759
775
|
console.log(`[${label}] attachment skipped: ${att.name} (${reason})`);
|
|
760
776
|
logEvent('attachment-skipped', { chat_id: chatId, msg_id: msg.message_id, name: att.name, reason });
|