polygram 0.9.0-rc.6 → 0.9.0
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/.claude-plugin/plugin.json +1 -1
- package/lib/error/classify.js +53 -0
- package/package.json +1 -1
- package/polygram.js +31 -4
|
@@ -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
|
|
4
|
+
"version": "0.9.0",
|
|
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",
|
package/lib/error/classify.js
CHANGED
|
@@ -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
|
|
3
|
+
"version": "0.9.0",
|
|
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
|
|
1029
|
-
// message starts fresh — without them having
|
|
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 })
|