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.20.3",
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",
@@ -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
@@ -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 = isAutoResumable({
340
- error: err, aborted: wasAborted, replay: isReplay, shuttingDown: isShuttingDown,
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 = errorReplyText(err);
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
- // Replace path separator with dash; leading dash signals absolute path.
90
- return cwd.replace(/\//g, '-');
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.20.3",
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.4.3",
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({ logger: console, sessionPrefix: 'polygram' }),
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({ logger: console, sessionPrefix: 'polygram' });
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,