@0xmaxma/claude-gateway 1.7.8 → 1.7.9

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.
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.SessionCompactor = exports.NotEnoughMessagesError = void 0;
37
+ exports.defaultSpawnClaude = defaultSpawnClaude;
37
38
  const fs = __importStar(require("fs"));
38
39
  const path = __importStar(require("path"));
39
40
  const child_process_1 = require("child_process");
@@ -55,15 +56,83 @@ const MERGE_SUMMARIES_PROMPT = 'These are summaries of sequential parts of a con
55
56
  const SINGLE_SUMMARY_INSTRUCTION = 'Summarize this conversation concisely, preserving key facts, decisions, context the assistant should remember, and any open questions or unfinished tasks.';
56
57
  // System prompt prepended to every claude CLI summarization call
57
58
  const COMPACTOR_SYSTEM_PROMPT = 'You are a conversation archiver. Your ONLY task is to produce a concise summary of the conversation transcript below. Do NOT respond to the conversation. Do NOT ask questions. Output ONLY the summary.';
59
+ // Max concurrent `claude --print` chunk-summary calls in flight at once. Bounded
60
+ // (not "all N chunks at once") to cap worst-case concurrent model calls/memory,
61
+ // while still cutting sequential wall time roughly by this factor on large sessions.
62
+ const CHUNK_CONCURRENCY = 4;
63
+ // Attempts per chunk (1 initial + retries) before degrading to a truncated raw
64
+ // excerpt instead of failing the whole compact job over one bad chunk.
65
+ const CHUNK_MAX_ATTEMPTS = 2;
66
+ // Raw-excerpt length used when a chunk exhausts its summarization attempts.
67
+ const DEGRADED_EXCERPT_CHARS = 2000;
68
+ // Hard ceiling on a single `claude --print` summarization spawn — mirrors the
69
+ // prior spawnSync timeout, but non-blocking (see defaultSpawnClaude below).
70
+ const SUMMARY_TIMEOUT_MS = 300000;
58
71
  // Rough token estimate: ~4 chars per token
59
72
  function estimateTokens(messages) {
60
73
  return Math.round(messages.reduce((acc, m) => acc + m.content.length, 0) / 4);
61
74
  }
75
+ /**
76
+ * Async `claude` CLI spawn. Replaces the prior `spawnSync` call: `spawnSync`
77
+ * blocks the daemon's single event loop for the full duration of the call — and
78
+ * every agent shares one process, so one compaction call froze ALL agents/
79
+ * channels until it returned (up to SUMMARY_TIMEOUT_MS, x N chunks sequentially
80
+ * on large sessions). `spawn` yields the event loop while the child runs.
81
+ * Mirrors the sync `{status, stdout, stderr, error}` contract so callers'
82
+ * existing error handling is unchanged.
83
+ */
84
+ function defaultSpawnClaude(bin, args, input) {
85
+ return new Promise((resolve) => {
86
+ let settled = false;
87
+ let stdout = '';
88
+ let stderr = '';
89
+ const settle = (r) => {
90
+ if (settled)
91
+ return;
92
+ settled = true;
93
+ clearTimeout(timer);
94
+ resolve(r);
95
+ };
96
+ const timer = setTimeout(() => {
97
+ try {
98
+ child.kill('SIGKILL');
99
+ }
100
+ catch {
101
+ /* already gone */
102
+ }
103
+ settle({ status: null, stdout: '', stderr, error: new Error(`claude CLI timed out after ${SUMMARY_TIMEOUT_MS}ms`) });
104
+ }, SUMMARY_TIMEOUT_MS);
105
+ const child = (0, child_process_1.spawn)(bin, args, {
106
+ stdio: ['pipe', 'pipe', 'pipe'],
107
+ env: { ...process.env, PATH: (0, claude_bin_1.pathWithNativeBin)() },
108
+ });
109
+ child.stdout?.on('data', (d) => {
110
+ stdout += String(d);
111
+ });
112
+ child.stderr?.on('data', (d) => {
113
+ stderr += String(d);
114
+ });
115
+ child.on('error', (err) => settle({ status: null, stdout: '', stderr, error: err }));
116
+ child.on('close', (code) => settle({ status: code, stdout, stderr }));
117
+ // stdin emits its own async 'error' (e.g. EPIPE if the child exits before we
118
+ // finish writing) separate from the child's 'error' event — must be handled
119
+ // or an unhandled stream error crashes the daemon.
120
+ child.stdin?.on('error', () => { });
121
+ try {
122
+ child.stdin?.write(input);
123
+ child.stdin?.end();
124
+ }
125
+ catch (err) {
126
+ settle({ status: null, stdout: '', stderr, error: err });
127
+ }
128
+ });
129
+ }
62
130
  class SessionCompactor {
63
- constructor(sessionStore) {
131
+ constructor(sessionStore, spawnClaude = defaultSpawnClaude) {
64
132
  this.sessionStore = sessionStore;
133
+ this.spawnClaude = spawnClaude;
65
134
  }
66
- async compact(agentId, chatId, sessionId, model, contextWindow, channel = 'telegram') {
135
+ async compact(agentId, chatId, sessionId, model, contextWindow, channel = 'telegram', onProgress) {
67
136
  // Load current history
68
137
  const messages = await this.sessionStore.loadTelegramSession(agentId, chatId, sessionId, channel);
69
138
  if (messages.length < 5) {
@@ -83,7 +152,7 @@ class SessionCompactor {
83
152
  const historyText = toSummarize
84
153
  .map(m => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`)
85
154
  .join('\n\n');
86
- const summaryText = await this.summarizeWithChunking(historyText, model);
155
+ const summaryText = await this.summarizeWithChunking(historyText, model, onProgress);
87
156
  const compacted = [
88
157
  { role: 'system', content: `[Conversation Summary]\n${summaryText}`, ts: Date.now() },
89
158
  ...tail,
@@ -97,21 +166,72 @@ class SessionCompactor {
97
166
  const contextPctAfter = Math.round((afterTokens / contextWindow) * 100);
98
167
  return { beforeMessages, afterMessages, beforeTokens, afterTokens, reductionPct, contextPctBefore, contextPctAfter };
99
168
  }
100
- async summarizeWithChunking(historyText, model) {
101
- // If small enough, summarize directly
169
+ async summarizeWithChunking(historyText, model, onProgress) {
170
+ // If small enough, summarize directly. No chunking → no chunk to retry/degrade
171
+ // against, so a failure here still fails the compact job (original behavior).
102
172
  if (historyText.length <= CHUNK_CHARS) {
103
173
  return this.callClaudeForSummary(historyText, model);
104
174
  }
105
- // Split into chunks and summarize each
175
+ // Split into chunks and summarize with bounded concurrency; a single bad
176
+ // chunk degrades to a raw excerpt instead of failing the whole job.
106
177
  const chunks = this.splitIntoChunks(historyText, CHUNK_CHARS);
107
- const chunkSummaries = [];
108
- for (let i = 0; i < chunks.length; i++) {
109
- const summary = await this.callClaudeForSummary(chunks[i], model, `This is part ${i + 1} of ${chunks.length} of a longer conversation. Summarize this segment concisely.`);
110
- chunkSummaries.push(`[Part ${i + 1}/${chunks.length}]\n${summary}`);
111
- }
112
- // Merge chunk summaries into final summary
178
+ const chunkSummaries = await this.summarizeChunksConcurrently(chunks, model, onProgress);
179
+ // Merge chunk summaries into final summary. If the merge call itself can't
180
+ // be completed, fall back to the concatenated per-part summaries rather than
181
+ // losing the whole compaction — still far smaller than the raw history.
113
182
  const mergedText = chunkSummaries.join('\n\n');
114
- return this.callClaudeForSummary(mergedText, model, MERGE_SUMMARIES_PROMPT);
183
+ return this.mergeChunkSummaries(mergedText, model);
184
+ }
185
+ /** Bounded worker pool (mirrors the pattern in apps/installer.ts restoreRunningApps): at
186
+ * most CHUNK_CONCURRENCY `claude --print` calls in flight; results land by index so
187
+ * output order matches chunk order regardless of completion order. */
188
+ async summarizeChunksConcurrently(chunks, model, onProgress) {
189
+ const results = new Array(chunks.length);
190
+ let completed = 0;
191
+ let cursor = 0;
192
+ const worker = async () => {
193
+ while (cursor < chunks.length) {
194
+ const i = cursor++;
195
+ results[i] = await this.summarizeChunkWithFallback(chunks[i], model, i, chunks.length);
196
+ completed++;
197
+ onProgress?.(completed, chunks.length);
198
+ }
199
+ };
200
+ const poolSize = Math.min(CHUNK_CONCURRENCY, chunks.length);
201
+ await Promise.all(Array.from({ length: poolSize }, () => worker()));
202
+ return results;
203
+ }
204
+ /** Summarize one chunk; after CHUNK_MAX_ATTEMPTS failures, degrade to a truncated raw
205
+ * excerpt instead of throwing, so one bad chunk can't fail the whole compact job. */
206
+ async summarizeChunkWithFallback(chunk, model, index, total) {
207
+ const instruction = `This is part ${index + 1} of ${total} of a longer conversation. Summarize this segment concisely.`;
208
+ let lastErr;
209
+ for (let attempt = 1; attempt <= CHUNK_MAX_ATTEMPTS; attempt++) {
210
+ try {
211
+ const summary = await this.callClaudeForSummary(chunk, model, instruction);
212
+ return `[Part ${index + 1}/${total}]\n${summary}`;
213
+ }
214
+ catch (err) {
215
+ lastErr = err;
216
+ }
217
+ }
218
+ const excerpt = chunk.length > DEGRADED_EXCERPT_CHARS ? `${chunk.slice(0, DEGRADED_EXCERPT_CHARS)}…(truncated)` : chunk;
219
+ console.error(`[SessionCompactor] Chunk ${index + 1}/${total} summarization failed after ${CHUNK_MAX_ATTEMPTS} attempts (${lastErr?.message ?? 'unknown error'}); using truncated raw excerpt.`);
220
+ return `[Part ${index + 1}/${total} — summarization failed after ${CHUNK_MAX_ATTEMPTS} attempts (${lastErr?.message ?? 'unknown error'}); showing truncated raw excerpt]\n${excerpt}`;
221
+ }
222
+ /** Merge per-chunk summaries into one; degrade to the unmerged concatenation on repeated failure. */
223
+ async mergeChunkSummaries(mergedText, model) {
224
+ let lastErr;
225
+ for (let attempt = 1; attempt <= CHUNK_MAX_ATTEMPTS; attempt++) {
226
+ try {
227
+ return await this.callClaudeForSummary(mergedText, model, MERGE_SUMMARIES_PROMPT);
228
+ }
229
+ catch (err) {
230
+ lastErr = err;
231
+ }
232
+ }
233
+ console.error(`[SessionCompactor] Merge-summaries call failed after ${CHUNK_MAX_ATTEMPTS} attempts (${lastErr?.message ?? 'unknown error'}); using unmerged per-chunk summaries.`);
234
+ return mergedText;
115
235
  }
116
236
  splitIntoChunks(text, maxChars) {
117
237
  const chunks = [];
@@ -139,12 +259,7 @@ class SessionCompactor {
139
259
  // spawn after the native-installer migration.
140
260
  const claudeBinRaw = process.env.CLAUDE_BIN ?? (0, claude_bin_1.resolveClaudeBin)().bin;
141
261
  const [claudeBin, ...claudeBinArgs] = claudeBinRaw.split(' ');
142
- const result = (0, child_process_1.spawnSync)(claudeBin, [...claudeBinArgs, '--print', '--model', model], {
143
- input: prompt,
144
- encoding: 'utf-8',
145
- timeout: 300000,
146
- env: { ...process.env, PATH: (0, claude_bin_1.pathWithNativeBin)() },
147
- });
262
+ const result = await this.spawnClaude(claudeBin, [...claudeBinArgs, '--print', '--model', model], prompt);
148
263
  if (result.error) {
149
264
  throw new Error(`claude CLI error: ${result.error.message}`);
150
265
  }
@@ -1 +1 @@
1
- {"version":3,"file":"compactor.js","sourceRoot":"","sources":["../../src/session/compactor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"compactor.js","sourceRoot":"","sources":["../../src/session/compactor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Cross-channel inbound media helpers.
3
+ *
4
+ * Shared by the channel webhook routers (LINE, Slack) so each one does not
5
+ * carry its own copy of the same magic-byte table.
6
+ */
7
+ /**
8
+ * Pick a file extension from an image's magic bytes; default jpg.
9
+ *
10
+ * Sniffing (rather than trusting a platform-supplied `mimetype`/filename) is
11
+ * deliberate: the field is attacker-controlled on every channel, and LINE does
12
+ * not send one at all.
13
+ */
14
+ export declare function sniffImageExt(buf: Buffer): string;
15
+ //# sourceMappingURL=image-sniff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-sniff.d.ts","sourceRoot":"","sources":["../../src/shared/image-sniff.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CASjD"}
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ /**
3
+ * Cross-channel inbound media helpers.
4
+ *
5
+ * Shared by the channel webhook routers (LINE, Slack) so each one does not
6
+ * carry its own copy of the same magic-byte table.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.sniffImageExt = sniffImageExt;
10
+ /**
11
+ * Pick a file extension from an image's magic bytes; default jpg.
12
+ *
13
+ * Sniffing (rather than trusting a platform-supplied `mimetype`/filename) is
14
+ * deliberate: the field is attacker-controlled on every channel, and LINE does
15
+ * not send one at all.
16
+ */
17
+ function sniffImageExt(buf) {
18
+ if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
19
+ return 'jpg';
20
+ if (buf.length >= 4 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47)
21
+ return 'png';
22
+ if (buf.length >= 3 && buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46)
23
+ return 'gif';
24
+ if (buf.length >= 12 &&
25
+ buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50)
26
+ return 'webp';
27
+ return 'jpg';
28
+ }
29
+ //# sourceMappingURL=image-sniff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-sniff.js","sourceRoot":"","sources":["../../src/shared/image-sniff.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;AASH,sCASC;AAhBD;;;;;;GAMG;AACH,SAAgB,aAAa,CAAC,GAAW;IACvC,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC3F,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9G,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC3F,IACE,GAAG,CAAC,MAAM,IAAI,EAAE;QAChB,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI;QAC1E,OAAO,MAAM,CAAC;IAChB,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -30,8 +30,7 @@ import { createMessageHandler } from './inbound';
30
30
  // the others see the O_EXCL marker and drop, so a stranger's DM is handled once.
31
31
  import { initDedupDir, isDuplicate, pruneDedup } from '../telegram/dedup';
32
32
  import type { DiscordMessageContext } from './types';
33
-
34
- const MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024;
33
+ import { MAX_ATTACHMENT_BYTES } from '../shared/limits';
35
34
 
36
35
  /** AgentRunner callback base (origin of CLAUDE_CHANNEL_CALLBACK, "" when unset).
37
36
  * Used to mint/approve `/cli` pairings via the runner, the same bridge the
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared limits for the channel reply tools (Discord / Telegram / Slack).
3
+ *
4
+ * Self-contained on purpose, like share-client.ts: mcp/** ships as source
5
+ * without src/**, so this module must not import from src/ (see
6
+ * tests/unit/mcp-no-src-imports.test.ts).
7
+ */
8
+
9
+ /**
10
+ * Cap on a single outbound attachment. 50 MB is the smallest of the three
11
+ * platforms' own upload ceilings, so one value keeps the tools' behaviour
12
+ * identical instead of each re-declaring the same literal.
13
+ */
14
+ export const MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024;
@@ -11,6 +11,7 @@
11
11
  * bot token at any time, so this module always sends directly from the
12
12
  * subprocess.
13
13
  */
14
+ import * as fs from 'fs';
14
15
  import type { ToolModule, McpToolDefinition, McpToolResult, ToolVisibility } from '../../types';
15
16
  // mcp/ ships as source (package.json `files` lists "mcp/", not "src/") and
16
17
  // runs directly under bun — it may only import a compiled dist/ artifact,
@@ -20,11 +21,17 @@ import type { ToolModule, McpToolDefinition, McpToolResult, ToolVisibility } fro
20
21
  // "Cannot find module" from an installed package). `npm run build` must have
21
22
  // run at least once for this import to resolve locally.
22
23
  import { SlackClient } from '../../../dist/api/slack-client.js';
24
+ import { MAX_ATTACHMENT_BYTES } from '../shared/limits';
23
25
 
24
26
  export class SlackModule implements ToolModule {
25
27
  id = 'slack';
26
28
  toolVisibility: ToolVisibility = 'current-channel';
27
29
 
30
+ // Files already delivered this session. Small models sometimes retry
31
+ // slack_reply after a transient send hiccup even though the upload
32
+ // succeeded, which spams duplicate images. We never re-send the same file.
33
+ private readonly sentFiles = new Set<string>();
34
+
28
35
  isEnabled(): boolean {
29
36
  return process.env.GATEWAY_ORIGIN_CHANNEL === 'slack';
30
37
  }
@@ -36,6 +43,7 @@ export class SlackModule implements ToolModule {
36
43
  description:
37
44
  'Send a reply to the current Slack conversation. ' +
38
45
  'Pass chat_id (the Slack channel/DM id shown in the <channel> tag) and text. ' +
46
+ 'Optionally pass files (absolute paths) to attach images or documents. ' +
39
47
  'Also pass message_id from the <channel> tag when present — it clears the ' +
40
48
  '⏳ "seen" reaction the gateway left on the inbound message. ' +
41
49
  'Pass thread_id to reply inside the same thread instead of top-level.',
@@ -50,6 +58,14 @@ export class SlackModule implements ToolModule {
50
58
  type: 'string',
51
59
  description: 'Message text.',
52
60
  },
61
+ files: {
62
+ type: 'array',
63
+ items: { type: 'string' },
64
+ description:
65
+ 'Absolute file paths to attach (e.g. a generate_image result path). ' +
66
+ 'Optional — text can be sent alone, files can be sent alone, or both ' +
67
+ 'together as one message with the text as caption.',
68
+ },
53
69
  message_id: {
54
70
  type: 'string',
55
71
  description:
@@ -63,7 +79,9 @@ export class SlackModule implements ToolModule {
63
79
  'to a threaded message stays in-thread.',
64
80
  },
65
81
  },
66
- required: ['chat_id', 'text'],
82
+ // `text` is NOT required: a files-only reply (an image with no caption)
83
+ // is a legitimate send.
84
+ required: ['chat_id'],
67
85
  },
68
86
  },
69
87
  ];
@@ -79,12 +97,26 @@ export class SlackModule implements ToolModule {
79
97
  const text = typeof args.text === 'string' ? args.text : '';
80
98
  const messageId = typeof args.message_id === 'string' ? args.message_id : '';
81
99
  const threadId = typeof args.thread_id === 'string' ? args.thread_id : '';
100
+ const requested = Array.isArray(args.files) ? (args.files as unknown[]) : [];
82
101
  const token = process.env.SLACK_BOT_TOKEN ?? '';
83
102
 
84
103
  if (!chatId) {
85
104
  return { content: [{ type: 'text', text: 'slack_reply: missing chat_id' }], isError: true };
86
105
  }
87
- if (!text) {
106
+
107
+ // Drop files already delivered successfully this session (retry-dedup): a
108
+ // small model sometimes retries slack_reply after a transient hiccup even
109
+ // though the upload landed, which would spam duplicate images.
110
+ const files = requested.filter(
111
+ (f): f is string => typeof f === 'string' && !this.sentFiles.has(f),
112
+ );
113
+
114
+ // Nothing new to say or send — the whole reply is a duplicate retry. No-op
115
+ // success so the agent treats it as delivered and stops retrying.
116
+ if (!text && files.length === 0 && requested.length > 0) {
117
+ return { content: [{ type: 'text', text: 'already sent (duplicate suppressed)' }] };
118
+ }
119
+ if (!text && files.length === 0) {
88
120
  return { content: [{ type: 'text', text: 'slack_reply: text cannot be empty' }], isError: true };
89
121
  }
90
122
  if (!token) {
@@ -93,16 +125,46 @@ export class SlackModule implements ToolModule {
93
125
 
94
126
  const client = new SlackClient({ botToken: token, logDir: process.env.GATEWAY_WORKSPACE_DIR ?? '/tmp' });
95
127
  try {
96
- const sent = await client.postMessage(chatId, text, threadId || undefined);
128
+ // Size-check before any upload starts, so an oversized file fails fast
129
+ // instead of half-way through a multi-file batch.
130
+ for (const f of files) {
131
+ const st = fs.statSync(f);
132
+ if (st.size > MAX_ATTACHMENT_BYTES) {
133
+ throw new Error(`file too large: ${f} (${(st.size / 1024 / 1024).toFixed(1)}MB, max 50MB)`);
134
+ }
135
+ }
136
+
137
+ // With files, ONE uploadFiles call carries both the attachments and the
138
+ // text (as Slack's initial_comment) — never also postMessage, which would
139
+ // split one reply into two Slack messages.
140
+ const sent =
141
+ files.length > 0
142
+ ? await client.uploadFiles(chatId, files, {
143
+ threadTs: threadId || undefined,
144
+ initialComment: text || undefined,
145
+ })
146
+ : await client.postMessage(chatId, text, threadId || undefined);
97
147
  if (!sent.ok) {
98
148
  return { content: [{ type: 'text', text: `slack_reply failed: ${sent.error}` }], isError: true };
99
149
  }
150
+ // Mark as sent only AFTER the send succeeds — a genuine failure leaves
151
+ // them eligible for a retry rather than silently dropped.
152
+ for (const f of files) this.sentFiles.add(f);
100
153
  // Best-effort: clear the ack-reaction the webhook left on the inbound
101
154
  // message. Never blocks or fails the reply itself on a reaction error.
102
155
  if (messageId) {
103
156
  void client.removeReaction(chatId, messageId).catch(() => {});
104
157
  }
105
- return { content: [{ type: 'text', text: 'Sent message to Slack.' }] };
158
+ return {
159
+ content: [
160
+ {
161
+ type: 'text',
162
+ text: files.length > 0
163
+ ? `Sent message to Slack (${files.length} file(s)).`
164
+ : 'Sent message to Slack.',
165
+ },
166
+ ],
167
+ };
106
168
  } catch (err) {
107
169
  return {
108
170
  content: [{ type: 'text', text: `slack_reply failed: ${(err as Error).message}` }],
@@ -8,6 +8,7 @@ import * as fs from 'fs';
8
8
  import * as os from 'os';
9
9
  import { migrateAccess, defaultAccess } from './pure';
10
10
  import { chunkText, htmlToPlain } from './typing';
11
+ import { MAX_ATTACHMENT_BYTES } from '../shared/limits';
11
12
  import type {
12
13
  ChannelModule,
13
14
  ChannelCapabilities,
@@ -20,7 +21,6 @@ import type {
20
21
  } from '../../types';
21
22
 
22
23
  const PHOTO_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);
23
- const MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024;
24
24
  const MAX_CHUNK_LIMIT = 4096;
25
25
 
26
26
  export class TelegramModule implements ChannelModule {
@@ -39,6 +39,7 @@ import { createIncidentStore } from '../../../dist/agent/incident-store.js'
39
39
  import type { RecoveryOutcome } from '../../../dist/agent/incident.js'
40
40
  import { initDedupDir, isDuplicate as _isDuplicate, pruneDedup as _pruneDedup } from './dedup'
41
41
  import { normalizeTelegramLineBreaks, resolveTelegramReplyFormat, migrateAccess, telegramDisplayName } from './pure'
42
+ import { MAX_ATTACHMENT_BYTES } from '../shared/limits'
42
43
 
43
44
  // Standalone fallback: default state dir to ~/.claude/channels/telegram
44
45
  const STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join(homedir(), '.claude', 'channels', 'telegram')
@@ -336,7 +337,6 @@ function defaultAccess(): Access {
336
337
  }
337
338
 
338
339
  const MAX_CHUNK_LIMIT = 4096
339
- const MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024
340
340
 
341
341
  // reply's files param takes any path. Claude can already Read+paste file
342
342
  // contents, so this isn't a new exfil channel for arbitrary paths — but the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xmaxma/claude-gateway",
3
- "version": "1.7.8",
3
+ "version": "1.7.9",
4
4
  "description": "Multi-agent gateway for Claude",
5
5
  "repository": {
6
6
  "type": "git",