polygram 0.20.3 → 0.22.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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.22.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/db/auto-resume.js
CHANGED
|
@@ -90,6 +90,9 @@ function createAutoResumeTracker({ cooldownMs = DEFAULT_COOLDOWN_MS, now = Date.
|
|
|
90
90
|
*/
|
|
91
91
|
function isAutoResumable({ error, aborted, replay, shuttingDown }) {
|
|
92
92
|
if (aborted || replay || shuttingDown) return false;
|
|
93
|
+
// Delivery may have happened before the contained process tree disappeared.
|
|
94
|
+
// Automatic continuation could duplicate work; a user resend starts fresh.
|
|
95
|
+
if (error?.code === 'SESSION_PROCESS_LOST') return false;
|
|
93
96
|
// Review F#6: channels analog of the tmux 'idle with no Claude activity'
|
|
94
97
|
// pattern. The bridge socket dropped mid-turn (claude crashed, bridge
|
|
95
98
|
// process died) — that's a wedge, not a runaway. Same intent as the
|
package/lib/error/classify.js
CHANGED
|
@@ -184,6 +184,15 @@ const CODES = {
|
|
|
184
184
|
isTransient: true,
|
|
185
185
|
autoRecover: null,
|
|
186
186
|
},
|
|
187
|
+
// The isolated process tree exited, so its persisted session cannot be
|
|
188
|
+
// resumed safely. The dispatcher clears that session before surfacing this
|
|
189
|
+
// guidance, and the user's resend starts a fresh process.
|
|
190
|
+
SESSION_PROCESS_LOST: {
|
|
191
|
+
kind: 'sessionProcessLost',
|
|
192
|
+
userMessage: '🔄 That Claude session stopped before the turn finished. Please resend your message and I\'ll start a fresh session.',
|
|
193
|
+
isTransient: false,
|
|
194
|
+
autoRecover: null,
|
|
195
|
+
},
|
|
187
196
|
// CHANNELS_HANDSHAKE_TIMEOUT: bridge process never sent session_init
|
|
188
197
|
// within the handshake window during start(). Usually means the bridge
|
|
189
198
|
// crashed pre-init or the socket file is stale.
|
|
@@ -194,6 +194,7 @@ function createDispatcher({
|
|
|
194
194
|
oomShutdown = getOomObservation()?.detected === true;
|
|
195
195
|
} catch {}
|
|
196
196
|
const isShuttingDown = getIsShuttingDown() || oomShutdown;
|
|
197
|
+
let processLossResetFailed = false;
|
|
197
198
|
logger.error?.(`[${sessionKey}] Error: ${err.message}`);
|
|
198
199
|
// Mark the row terminal so the right thing happens on next
|
|
199
200
|
// boot:
|
|
@@ -219,6 +220,26 @@ function createDispatcher({
|
|
|
219
220
|
replay: isReplay || undefined,
|
|
220
221
|
oom_shutdown: oomShutdown || undefined,
|
|
221
222
|
});
|
|
223
|
+
// A lost contained process cannot service a resume. Only promise a fresh
|
|
224
|
+
// session after the durable mapping has actually been removed.
|
|
225
|
+
if (err.code === 'SESSION_PROCESS_LOST') {
|
|
226
|
+
try {
|
|
227
|
+
if (typeof db.clearSessionId !== 'function') {
|
|
228
|
+
throw new Error('clearSessionId unavailable');
|
|
229
|
+
}
|
|
230
|
+
db.clearSessionId(sessionKey);
|
|
231
|
+
logEvent('session-reset-after-process-loss', {
|
|
232
|
+
chat_id: chatId, session_key: sessionKey, msg_id: msg?.message_id,
|
|
233
|
+
});
|
|
234
|
+
} catch (resetErr) {
|
|
235
|
+
processLossResetFailed = true;
|
|
236
|
+
logger.error?.(`[${sessionKey}] failed to clear lost session: ${resetErr.message}`);
|
|
237
|
+
logEvent('session-reset-after-process-loss-failed', {
|
|
238
|
+
chat_id: chatId, session_key: sessionKey, msg_id: msg?.message_id,
|
|
239
|
+
error: resetErr.message?.slice(0, 200),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
222
243
|
// Startup-gate death (claude exited during spawn / the dialog gate timed
|
|
223
244
|
// out) of a likely-aged RESUMED session — the persisted claude_session_id
|
|
224
245
|
// can't be resumed cleanly (shumorobot general chat 2026-06-01→03: a
|
|
@@ -336,9 +357,10 @@ function createDispatcher({
|
|
|
336
357
|
// (not handleMessage), so its errors don't re-enter this
|
|
337
358
|
// catch block — autoResumeTracker.isInCooldown() is the
|
|
338
359
|
// only guard needed against runaway loops.
|
|
339
|
-
const resumable =
|
|
340
|
-
|
|
341
|
-
|
|
360
|
+
const resumable = err.code !== 'SESSION_PROCESS_LOST'
|
|
361
|
+
&& isAutoResumable({
|
|
362
|
+
error: err, aborted: wasAborted, replay: isReplay, shuttingDown: isShuttingDown,
|
|
363
|
+
});
|
|
342
364
|
if (resumable && !autoResumeTracker.isInCooldown(sessionKey)) {
|
|
343
365
|
autoResumeTracker.markAttempt(sessionKey);
|
|
344
366
|
logEvent('auto-resume-attempted', {
|
|
@@ -393,7 +415,9 @@ function createDispatcher({
|
|
|
393
415
|
}
|
|
394
416
|
// 0.7.7: errorReplyText may return null (suppress reply
|
|
395
417
|
// signal — INTERRUPTED inside abort grace).
|
|
396
|
-
const replyText =
|
|
418
|
+
const replyText = err.code === 'SESSION_PROCESS_LOST' && processLossResetFailed
|
|
419
|
+
? '⚠️ That Claude session stopped, but I couldn\'t safely reset its saved state. Please try again later.'
|
|
420
|
+
: errorReplyText(err);
|
|
397
421
|
if (replyText) {
|
|
398
422
|
tg(bot, 'sendMessage', {
|
|
399
423
|
chat_id: chatId,
|
|
@@ -86,8 +86,9 @@ const os = require('os');
|
|
|
86
86
|
* → -Users-ivanshumkov-Projects-polygram
|
|
87
87
|
*/
|
|
88
88
|
function encodeCwd(cwd) {
|
|
89
|
-
//
|
|
90
|
-
|
|
89
|
+
// Claude replaces path separators and dots; the leading dash signals an
|
|
90
|
+
// absolute path.
|
|
91
|
+
return cwd.replace(/[/.]/g, '-');
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
// SECURITY (audit L3): sessionId is interpolated into a filesystem
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@anthropic-ai/claude-agent-sdk": "0.2.123",
|
|
65
65
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
66
|
-
"@shumkov/orchestra": "0.
|
|
66
|
+
"@shumkov/orchestra": "0.6.0",
|
|
67
67
|
"better-sqlite3": "^12.9.0",
|
|
68
68
|
"grammy": "^1.42.0",
|
|
69
69
|
"markdown-it": "^14.1.1",
|
package/polygram.js
CHANGED
|
@@ -2406,6 +2406,9 @@ async function main() {
|
|
|
2406
2406
|
loadStickers();
|
|
2407
2407
|
DB_PATH = dbOverride || path.join(DB_DIR, `${BOT_NAME}.db`);
|
|
2408
2408
|
console.log(`[polygram] bot: ${BOT_NAME} (${Object.keys(config.chats).length} chats) db: ${DB_PATH}`);
|
|
2409
|
+
const sessionLauncher = process.env.ORCHESTRA_SESSION_LAUNCHER;
|
|
2410
|
+
const requireExistingServer = process.env.ORCHESTRA_TMUX_REQUIRE_SERVER === '1';
|
|
2411
|
+
console.log(`[polygram] session containment configured: ${sessionLauncher ? 'yes' : 'no'}`);
|
|
2409
2412
|
|
|
2410
2413
|
// rc.50: claim our PID file BEFORE binding the bot token. If a
|
|
2411
2414
|
// prior daemon (orphan from a botched restart) is still running,
|
|
@@ -2429,7 +2432,11 @@ async function main() {
|
|
|
2429
2432
|
// sessions (dead safety net + EEXIST on cli respawn of a leaked session).
|
|
2430
2433
|
const sweep = await sweepTmuxOrphans({
|
|
2431
2434
|
botName: BOT_NAME,
|
|
2432
|
-
runner: createTmuxRunner({
|
|
2435
|
+
runner: createTmuxRunner({
|
|
2436
|
+
logger: console,
|
|
2437
|
+
sessionPrefix: 'polygram',
|
|
2438
|
+
requireExistingServer,
|
|
2439
|
+
}),
|
|
2433
2440
|
logger: console,
|
|
2434
2441
|
});
|
|
2435
2442
|
if (sweep.swept.length > 0) {
|
|
@@ -2739,7 +2746,11 @@ async function main() {
|
|
|
2739
2746
|
// tmux backend runner — one per daemon, shared across all TmuxProcess
|
|
2740
2747
|
// instances. Construction is cheap (no system call until first
|
|
2741
2748
|
// spawn/send). Only used if any chat in config has pm:'tmux'.
|
|
2742
|
-
const tmuxRunner = createTmuxRunner({
|
|
2749
|
+
const tmuxRunner = createTmuxRunner({
|
|
2750
|
+
logger: console,
|
|
2751
|
+
sessionPrefix: 'polygram',
|
|
2752
|
+
requireExistingServer,
|
|
2753
|
+
});
|
|
2743
2754
|
// Verify the pinned claude CLI binary is present. The tmux
|
|
2744
2755
|
// backend spawns this exact binary by absolute path (see
|
|
2745
2756
|
// lib/claude-bin.js + TmuxProcess.start) — it never resolves
|
|
@@ -2801,6 +2812,7 @@ async function main() {
|
|
|
2801
2812
|
db,
|
|
2802
2813
|
logger: console,
|
|
2803
2814
|
tmuxRunner,
|
|
2815
|
+
sessionLauncher,
|
|
2804
2816
|
botName: BOT_NAME,
|
|
2805
2817
|
// channels backend
|
|
2806
2818
|
toolDispatcher: channelsToolDispatcher,
|