@0xmaxma/claude-gateway 1.3.12 → 1.3.14

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.
@@ -4,6 +4,7 @@ import { type SkillRegistry } from '../skills';
4
4
  import { HistoryDB } from '../history/db';
5
5
  export declare const MAX_IMAGE_SIZE_BYTES: number;
6
6
  export declare const DEFAULT_MODELS: ModelConfig[];
7
+ export declare const CHANNEL_COALESCE_WINDOW_MS = 1200;
7
8
  export declare class AgentRunner extends EventEmitter {
8
9
  private agentConfig;
9
10
  private readonly gatewayConfig;
@@ -27,7 +28,11 @@ export declare class AgentRunner extends EventEmitter {
27
28
  private idleCleanerTimer;
28
29
  private readonly pendingApiSessions;
29
30
  private readonly sessionSpawnLocks;
31
+ private readonly tooLargeRecoveries;
32
+ private readonly tooLargeExhausted;
30
33
  private readonly pendingImagePaths;
34
+ private readonly channelCoalesce;
35
+ private readonly coalesceWindowMs;
31
36
  private readonly pendingApiAttachments;
32
37
  private readonly sessionHistory;
33
38
  private readonly apiChatIds;
@@ -60,6 +65,13 @@ export declare class AgentRunner extends EventEmitter {
60
65
  private persistModelToConfig;
61
66
  private static escapeXmlAttr;
62
67
  private resolveMediaPaths;
68
+ /**
69
+ * Inject one or more coalesced channel messages as a SINGLE turn. Each entry is
70
+ * still recorded individually (session history + permanent history DB), but their
71
+ * channel XML blocks are concatenated and delivered in one sendMessage() call so
72
+ * a photo and its instruction text are read together in the same turn.
73
+ */
74
+ private routeChannelTurn;
63
75
  private static buildChannelXml;
64
76
  private getOrSpawnSession;
65
77
  private spawnSession;
@@ -108,6 +120,23 @@ export declare class AgentRunner extends EventEmitter {
108
120
  * The process will be lazily re-spawned on the next incoming message.
109
121
  */
110
122
  private restartProcess;
123
+ /**
124
+ * Unified recovery for the recoverable "Request too large (max 32MB)" error.
125
+ * Reached from two backends that surface the SAME error differently:
126
+ * - PTY shell (headless=false): a `system/request_too_large` event emitted by
127
+ * the transcript tailer from the authoritative <synthetic> record (Bug A).
128
+ * - Headless (headless=true): claude --print emits a synthetic `result`
129
+ * (is_error + "Request too large (max"); the long-lived process otherwise
130
+ * stays alive and rejects every subsequent turn forever (Bug B).
131
+ *
132
+ * Each consecutive recovery shrinks the history re-injected on the next spawn
133
+ * (TOO_LARGE_HISTORY_LADDER: 50→40→30→20→10→0) so a pathological context drops
134
+ * under the 32MB ceiling. The respawn happens on the user's NEXT message (no
135
+ * auto-loop), and the counter resets on the next successful result. Once even a
136
+ * zero-history spawn still trips 32MB, stop escalating and ask the user to
137
+ * /clear rather than climb the ladder again.
138
+ */
139
+ private handleRequestTooLarge;
111
140
  /**
112
141
  * Stop all idle session subprocesses so they re-spawn with the latest
113
142
  * system prompt on the next incoming message.
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/agent/runner.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/agent/runner.ts"],"names":[],"mappings":"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"}
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.AgentRunner = exports.DEFAULT_MODELS = exports.MAX_IMAGE_SIZE_BYTES = void 0;
36
+ exports.AgentRunner = exports.CHANNEL_COALESCE_WINDOW_MS = exports.DEFAULT_MODELS = exports.MAX_IMAGE_SIZE_BYTES = void 0;
37
37
  const events_1 = require("events");
38
38
  const fs = __importStar(require("fs"));
39
39
  const fsPromises = __importStar(require("fs/promises"));
@@ -55,6 +55,15 @@ const cleanup_1 = require("../history/cleanup");
55
55
  const DEFAULT_IDLE_TIMEOUT_MINUTES = 30;
56
56
  const DEFAULT_MAX_CONCURRENT = 20;
57
57
  const ANTHROPIC_SOCKET_ERROR = 'socket connection was closed unexpectedly';
58
+ // History re-injection ladder for request_too_large (32MB) recovery. Index =
59
+ // number of consecutive 32MB recoveries on a session; the value is how many
60
+ // history messages the NEXT spawn re-injects. Index 0 is the healthy default
61
+ // (= MAX_HISTORY_MESSAGES, sourced from it so the two never drift); each retry
62
+ // steps down a rung, shrinking the re-loaded context until it drops under
63
+ // Anthropic's 32MB request ceiling. Past the last rung (0 history) the context
64
+ // can't shrink further, so the runner stops escalating and asks the user to
65
+ // /clear instead of looping forever.
66
+ const TOO_LARGE_HISTORY_LADDER = [process_1.MAX_HISTORY_MESSAGES, 40, 30, 20, 10, 0];
58
67
  exports.MAX_IMAGE_SIZE_BYTES = 10 * 1024 * 1024;
59
68
  exports.DEFAULT_MODELS = [
60
69
  { id: 'claude-opus-4-8', label: 'Opus 4.8', alias: 'opus', contextWindow: 1000000 },
@@ -68,6 +77,11 @@ const PROTECTED_WORKSPACE_FILES = [
68
77
  'IDENTITY.md', 'USER.md', 'HEARTBEAT.md',
69
78
  ];
70
79
  const MAX_API_IMAGES = 5;
80
+ // Trailing-edge window for coalescing channel messages that carry an image (or that
81
+ // arrive while an image is already buffered) into a single turn. Reset on every new
82
+ // related update, so an album burst or a client-split photo+caption is gathered
83
+ // whole. Plain text with no pending buffer bypasses this entirely (zero added latency).
84
+ exports.CHANNEL_COALESCE_WINDOW_MS = 1200;
71
85
  /**
72
86
  * Move UI-uploaded files from staging (ui-upload/) to permanent per-session storage
73
87
  * (media/api-{sessionId}/), matching the same pattern Telegram uses.
@@ -126,8 +140,24 @@ class AgentRunner extends events_1.EventEmitter {
126
140
  this.pendingApiSessions = new Set();
127
141
  // Serialises concurrent getOrSpawnSession calls for the same key to prevent double-spawn
128
142
  this.sessionSpawnLocks = new Map();
143
+ // Consecutive request_too_large (32MB) recoveries per mapKey. Survives the
144
+ // session respawn (SessionProcess is recreated on each restart) so the history
145
+ // re-injection can escalate-shrink across retries (TOO_LARGE_HISTORY_LADDER).
146
+ // Reset to 0 on the next successful result; read by spawnSession.
147
+ this.tooLargeRecoveries = new Map();
148
+ // mapKeys that exhausted the ladder (zero-history spawn still tripped 32MB).
149
+ // Once here, further 32MB events only re-notify "/clear" and do NOT restart —
150
+ // restarting would just churn a context that cannot shrink. Cleared together
151
+ // with the counter on a successful result and on /clear.
152
+ this.tooLargeExhausted = new Set();
129
153
  // Tracks pending Telegram image paths per chatId (queue) for size accumulation after each turn.
130
154
  this.pendingImagePaths = new Map();
155
+ // Coalescing buffer: collects channel messages that arrive close together so a
156
+ // photo and its instruction text (which Telegram may deliver as separate updates
157
+ // — long caption split, album burst, or photo sent before/after the text) are
158
+ // injected as ONE turn instead of two. Keyed by chatId. A trailing-edge timer
159
+ // flushes the buffer once no new related update has arrived for the window.
160
+ this.channelCoalesce = new Map();
131
161
  // Buffers attachment file paths registered via api_reply tool for the current API session turn.
132
162
  this.pendingApiAttachments = new Map();
133
163
  // Session history ring-buffer (last 10 spawned, newest first)
@@ -156,6 +186,8 @@ class AgentRunner extends events_1.EventEmitter {
156
186
  this.idleTimeoutMs =
157
187
  (agentConfig.session?.idleTimeoutMinutes ?? DEFAULT_IDLE_TIMEOUT_MINUTES) * 60 * 1000;
158
188
  this.maxConcurrent = agentConfig.session?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
189
+ this.coalesceWindowMs =
190
+ Number(process.env.CHANNEL_COALESCE_WINDOW_MS) || exports.CHANNEL_COALESCE_WINDOW_MS;
159
191
  }
160
192
  /**
161
193
  * Update the skill registry used for detecting /skill-name commands.
@@ -217,87 +249,33 @@ class AgentRunner extends events_1.EventEmitter {
217
249
  });
218
250
  return;
219
251
  }
220
- // Get active session ID and route message to it
221
- this.sessionStore.getActiveSessionId(this.agentConfig.id, chatId, channelSource)
222
- .then(async (sessionId) => {
223
- // Append user message to the active session
224
- const userContent = content || (meta['attachment_file_id'] ? '(photo)' : '');
225
- const userTs = Date.now();
226
- await this.sessionStore.appendTelegramMessage(this.agentConfig.id, chatId, sessionId, {
227
- role: 'user',
228
- content: userContent,
229
- ts: userTs,
230
- }, channelSource);
231
- // Persist to permanent history DB (separate from session context)
232
- const mediaFiles = [];
233
- if (meta['image_path']) {
234
- try {
235
- const rel = media_store_1.MediaStore.copyToMedia(this.agentsBaseDir, this.agentConfig.id, `${channelSource}-${chatId}`, meta['image_path']);
236
- mediaFiles.push(rel);
237
- }
238
- catch {
239
- // Non-fatal — continue without media
240
- }
241
- }
242
- this.historyDb.insertMessage({
243
- chatId: `${channelSource}-${chatId}`,
244
- sessionId,
245
- source: channelSource,
246
- role: 'user',
247
- content: userContent,
248
- senderName: meta['user'] ?? undefined,
249
- senderId: meta['user_id'] ?? meta['chat_id'] ?? undefined,
250
- platformMessageId: meta['message_id'] ?? undefined,
251
- mediaFiles: mediaFiles.length > 0 ? mediaFiles : undefined,
252
- ts: userTs,
253
- });
254
- // Restart session before this turn if accumulated image size exceeded threshold
255
- if (this.pendingRestarts.has(chatId)) {
256
- const existingSession = this.sessions.get(chatId);
257
- if (existingSession) {
258
- await existingSession.stop();
259
- this.sessions.delete(chatId);
260
- }
261
- this.pendingRestarts.delete(chatId);
262
- this.imageSizePerChat.delete(chatId);
263
- }
264
- // Route to session process (map key = chatId, actual sessionId passed separately)
265
- // Channel sessions use agent-level model (not per-session)
266
- const session = await this.getOrSpawnSession(chatId, channelSource, sessionId);
267
- let channelXml = AgentRunner.buildChannelXml(params);
268
- // Detect skill commands and inject skill content
269
- const skillInvocation = (0, skills_1.detectSkillCommand)(content, this.skillRegistry);
270
- if (skillInvocation) {
271
- channelXml += (0, skills_1.formatSkillContext)(skillInvocation);
272
- this.logger.info('Skill invoked', {
273
- skill: skillInvocation.skillKey,
274
- args: skillInvocation.args,
275
- chatId,
276
- });
277
- }
278
- const imagePath = meta['image_path'];
279
- if (imagePath) {
280
- const queue = this.pendingImagePaths.get(chatId) ?? [];
281
- queue.push(imagePath);
282
- this.pendingImagePaths.set(chatId, queue);
283
- }
284
- session.setProcessing(true);
285
- session.sendMessage(channelXml);
286
- session.touch();
287
- this.logger.debug('Injected channel turn into session', {
288
- chatId,
289
- sessionId,
290
- user: meta['user'],
291
- });
292
- })
293
- .catch((err) => {
294
- this.logger.error('Failed to route message to session', {
295
- chatId,
296
- error: err.message,
297
- });
298
- const code = err.message.includes('pool full') ? 'POOL_FULL' : 'SPAWN_FAILED';
299
- this.writeTypingError(chatId, code);
300
- });
252
+ // Coalesce messages that carry an image (or that arrive while an image is
253
+ // already buffered) so a photo and its instruction text are injected as
254
+ // ONE turn — Telegram can split them across updates (long-caption split,
255
+ // album burst, or photo sent just before/after the text). Plain text with
256
+ // nothing buffered takes the fast path and is injected immediately.
257
+ const hasImage = !!meta['image_path'];
258
+ const pending = this.channelCoalesce.get(chatId);
259
+ if (hasImage || pending) {
260
+ const buf = pending ?? {
261
+ channelSource,
262
+ entries: [],
263
+ timer: undefined,
264
+ };
265
+ buf.channelSource = channelSource;
266
+ buf.entries.push(params);
267
+ if (buf.timer)
268
+ clearTimeout(buf.timer);
269
+ buf.timer = setTimeout(() => {
270
+ const flushed = this.channelCoalesce.get(chatId);
271
+ this.channelCoalesce.delete(chatId);
272
+ if (flushed)
273
+ this.routeChannelTurn(chatId, flushed.channelSource, flushed.entries);
274
+ }, this.coalesceWindowMs);
275
+ this.channelCoalesce.set(chatId, buf);
276
+ return;
277
+ }
278
+ this.routeChannelTurn(chatId, channelSource, [params]);
301
279
  }
302
280
  catch (err) {
303
281
  this.logger.warn('Failed to parse channel callback body', {
@@ -518,6 +496,105 @@ class AgentRunner extends events_1.EventEmitter {
518
496
  }
519
497
  return paths;
520
498
  }
499
+ /**
500
+ * Inject one or more coalesced channel messages as a SINGLE turn. Each entry is
501
+ * still recorded individually (session history + permanent history DB), but their
502
+ * channel XML blocks are concatenated and delivered in one sendMessage() call so
503
+ * a photo and its instruction text are read together in the same turn.
504
+ */
505
+ routeChannelTurn(chatId, channelSource, entries) {
506
+ if (entries.length === 0)
507
+ return;
508
+ this.sessionStore.getActiveSessionId(this.agentConfig.id, chatId, channelSource)
509
+ .then(async (sessionId) => {
510
+ // Record each buffered message individually (history + session context).
511
+ for (const entry of entries) {
512
+ const meta = entry.meta ?? {};
513
+ const content = entry.content ?? '';
514
+ const userContent = content || (meta['attachment_file_id'] || meta['image_path'] ? '(photo)' : '');
515
+ const userTs = Date.now();
516
+ await this.sessionStore.appendTelegramMessage(this.agentConfig.id, chatId, sessionId, {
517
+ role: 'user',
518
+ content: userContent,
519
+ ts: userTs,
520
+ }, channelSource);
521
+ // Persist to permanent history DB (separate from session context)
522
+ const mediaFiles = [];
523
+ if (meta['image_path']) {
524
+ try {
525
+ const rel = media_store_1.MediaStore.copyToMedia(this.agentsBaseDir, this.agentConfig.id, `${channelSource}-${chatId}`, meta['image_path']);
526
+ mediaFiles.push(rel);
527
+ }
528
+ catch {
529
+ // Non-fatal — continue without media
530
+ }
531
+ }
532
+ this.historyDb.insertMessage({
533
+ chatId: `${channelSource}-${chatId}`,
534
+ sessionId,
535
+ source: channelSource,
536
+ role: 'user',
537
+ content: userContent,
538
+ senderName: meta['user'] ?? undefined,
539
+ senderId: meta['user_id'] ?? meta['chat_id'] ?? undefined,
540
+ platformMessageId: meta['message_id'] ?? undefined,
541
+ mediaFiles: mediaFiles.length > 0 ? mediaFiles : undefined,
542
+ ts: userTs,
543
+ });
544
+ }
545
+ // Restart session before this turn if accumulated image size exceeded threshold
546
+ if (this.pendingRestarts.has(chatId)) {
547
+ const existingSession = this.sessions.get(chatId);
548
+ if (existingSession) {
549
+ await existingSession.stop();
550
+ this.sessions.delete(chatId);
551
+ }
552
+ this.pendingRestarts.delete(chatId);
553
+ this.imageSizePerChat.delete(chatId);
554
+ }
555
+ // Route to session process (map key = chatId, actual sessionId passed separately)
556
+ // Channel sessions use agent-level model (not per-session)
557
+ const session = await this.getOrSpawnSession(chatId, channelSource, sessionId);
558
+ // Merge buffered messages into one turn: concat channel XML blocks, append
559
+ // any skill context, and accumulate image paths for size tracking.
560
+ const blocks = [];
561
+ for (const entry of entries) {
562
+ let channelXml = AgentRunner.buildChannelXml(entry);
563
+ const skillInvocation = (0, skills_1.detectSkillCommand)(entry.content ?? '', this.skillRegistry);
564
+ if (skillInvocation) {
565
+ channelXml += (0, skills_1.formatSkillContext)(skillInvocation);
566
+ this.logger.info('Skill invoked', {
567
+ skill: skillInvocation.skillKey,
568
+ args: skillInvocation.args,
569
+ chatId,
570
+ });
571
+ }
572
+ blocks.push(channelXml);
573
+ const imagePath = entry.meta?.['image_path'];
574
+ if (imagePath) {
575
+ const queue = this.pendingImagePaths.get(chatId) ?? [];
576
+ queue.push(imagePath);
577
+ this.pendingImagePaths.set(chatId, queue);
578
+ }
579
+ }
580
+ session.setProcessing(true);
581
+ session.sendMessage(blocks.join('\n'));
582
+ session.touch();
583
+ this.logger.debug('Injected channel turn into session', {
584
+ chatId,
585
+ sessionId,
586
+ messages: entries.length,
587
+ });
588
+ })
589
+ .catch((err) => {
590
+ this.logger.error('Failed to route message to session', {
591
+ chatId,
592
+ error: err.message,
593
+ });
594
+ const code = err.message.includes('pool full') ? 'POOL_FULL' : 'SPAWN_FAILED';
595
+ this.writeTypingError(chatId, code);
596
+ });
597
+ }
521
598
  static buildChannelXml(params) {
522
599
  const meta = params.meta ?? {};
523
600
  const optionalAttrs = [
@@ -616,6 +693,19 @@ class AgentRunner extends events_1.EventEmitter {
616
693
  // Apply per-session model override (caller already normalized to undefined when == agent default)
617
694
  if (modelOverride)
618
695
  proc.modelOverride = modelOverride;
696
+ // request_too_large (32MB) recovery: shrink the re-injected history on each
697
+ // consecutive retry so a pathological context eventually fits. recoveryCount
698
+ // is 0 for healthy sessions → ladder rung 0 = MAX_HISTORY_MESSAGES (same as
699
+ // the SessionProcess default), so applying it unconditionally is a no-op for
700
+ // healthy sessions and the only path that sets historyLimit for recovering ones.
701
+ const recoveryCount = this.tooLargeRecoveries.get(mapKey) ?? 0;
702
+ const ladderIdx = Math.min(recoveryCount, TOO_LARGE_HISTORY_LADDER.length - 1);
703
+ proc.historyLimit = TOO_LARGE_HISTORY_LADDER[ladderIdx];
704
+ if (recoveryCount > 0) {
705
+ this.logger.info('Spawning with reduced history after request_too_large', {
706
+ mapKey, recoveryCount, historyLimit: proc.historyLimit,
707
+ });
708
+ }
619
709
  await proc.start();
620
710
  // Forward all session output lines so listeners on AgentRunner (GatewayRouter,
621
711
  // CronScheduler, tests) receive them without needing individual session references.
@@ -709,23 +799,11 @@ class AgentRunner extends events_1.EventEmitter {
709
799
  }
710
800
  }
711
801
  // PTY shell hit the recoverable "Request too large (max 32MB)" error.
712
- // The wrapper already dismissed the TUI overlay (double-ESC). Restart the
713
- // session so the oversized in-memory context (large images/files) is
714
- // dropped a fresh process reloads the much smaller text-only history
715
- // from the store, so the next message isn't doomed to re-hit 32MB. The
716
- // proc exit handler clears the typing indicator; tell the user to resend.
802
+ // The transcript tailer emits this from the authoritative <synthetic>
803
+ // record (Bug A fix). Recovery is unified in handleRequestTooLarge()
804
+ // so the headless path (Bug B) gets identical treatment.
717
805
  if (obj['type'] === 'system' && obj['subtype'] === 'request_too_large') {
718
- this.logger.warn('Request too large (32MB) — restarting session to clear oversized context', { mapKey });
719
- proc.setProcessing(false);
720
- // Ordering matters and makes the notice delivery race-free: writeAutoForward
721
- // persists the `.forward` file synchronously HERE, before restartProcess()
722
- // begins its async stop/kill. The typing-loop tear-down (stop() in typing.ts)
723
- // drains and sends `.forward` synchronously before it removes the typing
724
- // signal, so by the time the proc exit fires and the loop stops, the file is
725
- // already on disk and is delivered — no dependency on poll timing. Do not
726
- // reorder restartProcess() ahead of writeAutoForward().
727
- this.writeAutoForward(mapKey, '⚠️ Context too large — hit Anthropic\'s 32MB request limit (usually from large images or files in context). Restarting this session with a fresh context. Your last message was not processed — please resend it.');
728
- void this.restartProcess(mapKey);
806
+ this.handleRequestTooLarge(mapKey, proc);
729
807
  }
730
808
  if (obj['type'] === 'result') {
731
809
  proc.setProcessing(false);
@@ -767,10 +845,22 @@ class AgentRunner extends events_1.EventEmitter {
767
845
  if (isSocketError) {
768
846
  this.writeAutoForward(mapKey, '⚡ Connection to Anthropic API dropped. Please resend your message.');
769
847
  }
770
- // Suppress the "Request too large (max 32MB)" result: the dedicated
771
- // request_too_large event already sent a friendly notice and kicked off
772
- // a restart, so the raw TUI error text must not be forwarded as a duplicate.
848
+ // Bug B: headless (claude --print) reports a too-large request as a
849
+ // synthetic `result` (is_error + "Request too large (max"), NOT as the
850
+ // PTY `system/request_too_large` event. Previously this result was only
851
+ // suppressed — the long-lived process stayed alive and rejected every
852
+ // turn forever ("typing then silent"). Route it through the SAME unified
853
+ // recovery (notify + escalate-shrink history + restart) as the PTY path.
773
854
  const isRequestTooLarge = obj['is_error'] === true && resultText.includes(screen_1.TUI_REQUEST_TOO_LARGE);
855
+ if (isRequestTooLarge) {
856
+ this.handleRequestTooLarge(mapKey, proc);
857
+ }
858
+ else if (obj['is_error'] !== true) {
859
+ // Genuine successful turn — clear any request_too_large escalation so
860
+ // the next 32MB (if any) starts fresh at the top of the ladder.
861
+ this.tooLargeRecoveries.delete(mapKey);
862
+ this.tooLargeExhausted.delete(mapKey);
863
+ }
774
864
  // Skip when a menu was rendered to buttons this turn — the result
775
865
  // text is the same numbered list and would duplicate the menu message.
776
866
  if (!isSocketError && !isRequestTooLarge && resultText.trim() && !proc.queryMode && !replyCalled && !isThinkingCorruption && !menuSentThisTurn) {
@@ -1027,6 +1117,10 @@ class AgentRunner extends events_1.EventEmitter {
1027
1117
  this.historyDb.clearChat(historyChatId);
1028
1118
  // Delete persisted media files for this chat
1029
1119
  media_store_1.MediaStore.clearChatMedia(this.agentsBaseDir, agentId, historyChatId);
1120
+ // Reset any request_too_large escalation — the context is now empty, so the
1121
+ // next spawn should start fresh at the top of the history ladder.
1122
+ this.tooLargeRecoveries.delete(chatId);
1123
+ this.tooLargeExhausted.delete(chatId);
1030
1124
  // Kill old process so next message spawns fresh
1031
1125
  this.restartProcess(chatId).catch(() => { });
1032
1126
  }
@@ -1098,6 +1192,54 @@ class AgentRunner extends events_1.EventEmitter {
1098
1192
  }
1099
1193
  // Process will be re-spawned on next incoming message
1100
1194
  }
1195
+ /**
1196
+ * Unified recovery for the recoverable "Request too large (max 32MB)" error.
1197
+ * Reached from two backends that surface the SAME error differently:
1198
+ * - PTY shell (headless=false): a `system/request_too_large` event emitted by
1199
+ * the transcript tailer from the authoritative <synthetic> record (Bug A).
1200
+ * - Headless (headless=true): claude --print emits a synthetic `result`
1201
+ * (is_error + "Request too large (max"); the long-lived process otherwise
1202
+ * stays alive and rejects every subsequent turn forever (Bug B).
1203
+ *
1204
+ * Each consecutive recovery shrinks the history re-injected on the next spawn
1205
+ * (TOO_LARGE_HISTORY_LADDER: 50→40→30→20→10→0) so a pathological context drops
1206
+ * under the 32MB ceiling. The respawn happens on the user's NEXT message (no
1207
+ * auto-loop), and the counter resets on the next successful result. Once even a
1208
+ * zero-history spawn still trips 32MB, stop escalating and ask the user to
1209
+ * /clear rather than climb the ladder again.
1210
+ */
1211
+ handleRequestTooLarge(mapKey, proc) {
1212
+ proc.setProcessing(false);
1213
+ const count = (this.tooLargeRecoveries.get(mapKey) ?? 0) + 1;
1214
+ const lastRung = TOO_LARGE_HISTORY_LADDER.length - 1; // index of the 0-history rung
1215
+ if (count > lastRung) {
1216
+ // Even the zero-history spawn tripped 32MB — context can't shrink further.
1217
+ // Pin the counter at the last rung and always surface the /clear next step.
1218
+ this.tooLargeRecoveries.set(mapKey, lastRung);
1219
+ this.logger.error('Request too large persists with zero re-injected history', { mapKey, count });
1220
+ this.writeAutoForward(mapKey, '⚠️ ยังเกิน 32MB แม้จะล้าง context จนว่างแล้ว — พิมพ์ /clear เพื่อเริ่มเซสชันใหม่ หรือ /restart ค่ะ');
1221
+ // Restart ONCE to clear the wedged process the first time the ladder is
1222
+ // exhausted; thereafter stop restarting (no churn) and let the user /clear.
1223
+ if (!this.tooLargeExhausted.has(mapKey)) {
1224
+ this.tooLargeExhausted.add(mapKey);
1225
+ void this.restartProcess(mapKey);
1226
+ }
1227
+ return;
1228
+ }
1229
+ this.tooLargeRecoveries.set(mapKey, count);
1230
+ this.logger.warn('Request too large (32MB) — restarting with reduced history', {
1231
+ mapKey, attempt: count, nextHistoryLimit: TOO_LARGE_HISTORY_LADDER[count],
1232
+ });
1233
+ // Ordering matters and makes the notice delivery race-free: writeAutoForward
1234
+ // persists the `.forward` file synchronously HERE, before restartProcess()
1235
+ // begins its async stop/kill. The typing-loop tear-down (stop() in typing.ts)
1236
+ // drains and sends `.forward` synchronously before it removes the typing
1237
+ // signal, so by the time the proc exit fires and the loop stops, the file is
1238
+ // already on disk and is delivered — no dependency on poll timing. Do not
1239
+ // reorder restartProcess() ahead of writeAutoForward().
1240
+ this.writeAutoForward(mapKey, '⚠️ Context too large — hit Anthropic\'s 32MB request limit (usually from large images or files in context). Restarting this session with a fresh context. Your last message was not processed — please resend it.');
1241
+ void this.restartProcess(mapKey);
1242
+ }
1101
1243
  /**
1102
1244
  * Stop all idle session subprocesses so they re-spawn with the latest
1103
1245
  * system prompt on the next incoming message.
@@ -1326,6 +1468,10 @@ class AgentRunner extends events_1.EventEmitter {
1326
1468
  srv.closeAllConnections?.();
1327
1469
  await new Promise((resolve) => srv.close(() => resolve()));
1328
1470
  }
1471
+ for (const buf of this.channelCoalesce.values()) {
1472
+ clearTimeout(buf.timer);
1473
+ }
1474
+ this.channelCoalesce.clear();
1329
1475
  this.receiver?.stop();
1330
1476
  this.discordReceiver?.stop();
1331
1477
  await Promise.all([...this.sessions.values()].map((s) => s.stop()));