polygram 0.9.0-rc.6 → 0.9.0-rc.7

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "polygram",
4
- "version": "0.9.0-rc.6",
4
+ "version": "0.9.0-rc.7",
5
5
  "description": "Telegram integration for Claude Code that preserves the OpenClaw per-chat session model. Migration target for OpenClaw users. Multi-bot, multi-chat, per-topic isolation; SQLite transcripts; inline-keyboard approvals. Bundles /polygram:status|logs|pair-code|approvals admin commands plus history (transcript queries) and polygram-send (out-of-turn IPC sends with file-upload validation) skills.",
6
6
  "keywords": [
7
7
  "telegram",
@@ -60,6 +60,20 @@ const PATTERNS = {
60
60
  // "input...missing" or "missing...input" within a tool_use mention.
61
61
  missingToolInput: /tool[_ ]use.*(input.*missing|missing.*input)|missing tool call input|tool input required/i,
62
62
 
63
+ // Anthropic API rejected an image content block in the conversation.
64
+ // 2026-05-13: shumabit Dina DM hit this after accumulating 53 images
65
+ // over 2 weeks — every new turn 400'd with raw API JSON dumped to
66
+ // the user. Most common cause: a persisted image in the resumed
67
+ // transcript that the API now considers invalid (model snapshot
68
+ // drift, expired URL, bad base64, dimension/size cap, format the
69
+ // API stopped accepting). Recovery: reset_session — /compact has
70
+ // to load the same history so it usually fails too.
71
+ //
72
+ // Anchor on the literal Anthropic phrase to avoid false positives
73
+ // on routine "image" mentions. Tightened by requiring an
74
+ // image-failure verb co-located with "image" or "photo".
75
+ imageProcess: /(could not process|cannot process|failed to (process|load|decode)|unsupported|invalid|corrupt(?:ed)?)[^\n]{0,80}\b(image|photo)\b|\b(image|photo)\b[^\n]{0,80}(could not process|failed to (process|load|decode)|is (invalid|corrupted|unsupported))/i,
76
+
63
77
  // Idle/wall-clock timeout from polygram's pm timers, OR
64
78
  // model-side timeout. Mapped to a single class; user message is
65
79
  // identical either way.
@@ -87,6 +101,7 @@ const USER_MESSAGES = {
87
101
  contextOverflow: '📚 Conversation got too long. Send /new to start fresh.',
88
102
  roleOrdering: '⚠️ Conversation got into a tangled state. Try /new.',
89
103
  missingToolInput: '⚠️ Session history looks corrupted. Try /new.',
104
+ imageProcess: '🖼 One of the images in this conversation can\'t be re-processed by Claude — likely an older one in the history. Starting a fresh session for this chat.',
90
105
  timeout: '⏳ I went quiet too long without finishing. Try resending or simplifying.',
91
106
  format: '⚠️ Invalid request format. Try rephrasing or /new.',
92
107
  // Used both for in-flight retry attempts AND for the post-retry-failed
@@ -106,6 +121,7 @@ const AUTO_RECOVER = {
106
121
  roleOrdering: 'reset_session',
107
122
  contextOverflow: 'reset_session',
108
123
  missingToolInput: 'reset_session',
124
+ imageProcess: 'reset_session',
109
125
  };
110
126
 
111
127
  // Typed-code short-circuits — set on errors polygram throws itself
@@ -300,8 +316,45 @@ function isTransientHttpError(err) {
300
316
  return classify(err).isTransient;
301
317
  }
302
318
 
319
+ // 2026-05-13: detect the case where SDK reports result_subtype=success
320
+ // BUT the assistant text is actually an API error JSON the SDK wrapped
321
+ // as if it were the model's reply. Happens when the resumed session's
322
+ // transcript has data the API can't reload — most commonly an image
323
+ // content block. Pattern is distinctive ("API Error: <code> {...
324
+ // type:error ...}") so false positives are unlikely.
325
+ //
326
+ // When this matches:
327
+ // - The text we'd deliver to Telegram is garbage (raw JSON).
328
+ // - The Claude session is wedged — every future turn will fail the
329
+ // same way until reset.
330
+ // Returns the same shape as classify() so callers can use it uniformly,
331
+ // or null when the text looks like a legitimate assistant reply.
332
+ const WEDGED_SESSION_RE = /^API Error: \d{3}\s+\{[^}]*"type"\s*:\s*"error"/;
333
+
334
+ function detectWedgedSessionError(text) {
335
+ if (typeof text !== 'string' || text.length === 0) return null;
336
+ if (!WEDGED_SESSION_RE.test(text)) return null;
337
+ // Once we've confirmed it's a wrapped API error, run classify on the
338
+ // text — it picks up imageProcess / rateLimit / billing / etc. from
339
+ // the JSON body. classify is robust to JSON-ish input.
340
+ const cls = classify(new Error(text));
341
+ // If classify fell through to 'unknown', return a safe imageProcess
342
+ // shape — wedged sessions are most commonly image-driven and the
343
+ // recovery action (reset_session) is correct for all wedge classes.
344
+ if (cls.kind === 'unknown') {
345
+ return {
346
+ kind: 'imageProcess',
347
+ userMessage: USER_MESSAGES.imageProcess,
348
+ isTransient: false,
349
+ autoRecover: 'reset_session',
350
+ };
351
+ }
352
+ return cls;
353
+ }
354
+
303
355
  module.exports = {
304
356
  classify,
357
+ detectWedgedSessionError,
305
358
  isTransientHttpError,
306
359
  PATTERNS,
307
360
  USER_MESSAGES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.9.0-rc.6",
3
+ "version": "0.9.0-rc.7",
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
@@ -75,7 +75,7 @@ const { startTyping } = require('./lib/telegram/typing');
75
75
  // consumer is lib/handlers/download.js.
76
76
  const { createReactionManager, classifyToolName } = require('./lib/telegram/reactions');
77
77
  const { createMediaGroupBuffer } = require('./lib/media-group-buffer');
78
- const { classify: classifyError, isTransientHttpError } = require('./lib/error/classify');
78
+ const { classify: classifyError, detectWedgedSessionError, isTransientHttpError } = require('./lib/error/classify');
79
79
  const { createAutoResumeTracker, isAutoResumable } = require('./lib/db/auto-resume');
80
80
  const { resolveReplayWindowMs } = require('./lib/db/replay-window');
81
81
  // validateIpcFileParam moved with handleSendOverIpc to
@@ -1019,14 +1019,41 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
1019
1019
 
1020
1020
  stopTyping();
1021
1021
 
1022
+ // 2026-05-13 Dina-DM incident: SDK reports result_subtype=success
1023
+ // but the "assistant text" is actually an API error JSON the SDK
1024
+ // wrapped (e.g. wedged image content block in resumed transcript).
1025
+ // Without this guard, polygram delivers the raw JSON to Telegram
1026
+ // as the bot's reply AND never resets the session — every
1027
+ // subsequent turn loops the same wedge.
1028
+ //
1029
+ // Sniff result.text for the wrapper pattern BEFORE the error/text
1030
+ // branch decisions. When detected: synthesize an Error so the
1031
+ // standard result.error path runs (classification → reset_session
1032
+ // → friendly user message → no raw-JSON delivery).
1033
+ if (!result.error && detectWedgedSessionError(result.text)) {
1034
+ const wedge = detectWedgedSessionError(result.text);
1035
+ logEvent('wedged-session-detected', {
1036
+ chat_id: chatId, session_key: sessionKey,
1037
+ kind: wedge.kind,
1038
+ text_preview: result.text.slice(0, 200),
1039
+ });
1040
+ // Promote the wrapped error to result.error so the existing
1041
+ // auto-recover + thrown-error machinery handles it uniformly.
1042
+ // Clear result.text so downstream delivery doesn't send the
1043
+ // raw JSON.
1044
+ result.error = result.text;
1045
+ result.text = '';
1046
+ }
1047
+
1022
1048
  if (result.error) {
1023
1049
  console.error(`[${label}] Error (${elapsed}s):`, result.error);
1024
1050
  reactor.setState('ERROR');
1025
1051
  // 0.8.0 Phase 2 step 8: classifier-driven auto-recovery. If
1026
1052
  // the error kind has autoRecover === 'reset_session' (i.e.
1027
- // role_ordering / context_overflow / missing_tool_input),
1028
- // tell pm to reset the session NOW so the user's NEXT
1029
- // message starts fresh — without them having to type /new.
1053
+ // role_ordering / context_overflow / missing_tool_input /
1054
+ // imageProcess), tell pm to reset the session NOW so the
1055
+ // user's NEXT message starts fresh — without them having
1056
+ // to type /new.
1030
1057
  const cls = classifyError(result.error);
1031
1058
  if (cls.autoRecover === 'reset_session') {
1032
1059
  pm.resetSession(sessionKey, { reason: cls.kind })