polygram 0.6.4 → 0.6.5

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/polygram.js +38 -40
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
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
@@ -756,12 +756,6 @@ function dispatchHandleMessage(sessionKey, chatId, msg, bot) {
756
756
  });
757
757
  }
758
758
 
759
- // drainQueuesForChat is retained as a no-op for backwards compat with
760
- // call sites in /model, /effort, chat-migration, and abort handlers.
761
- // Returns 0 always; a drain isn't meaningful in the concurrent model —
762
- // callers that want to abort should rely on pm.killChat.
763
- const drainQueuesForChat = (_chatId) => 0;
764
-
765
759
  // Per-session lock ordering stdin writes. Module is I/O-pure.
766
760
  const stdinLock = createAsyncLock();
767
761
 
@@ -1161,17 +1155,17 @@ async function handleConfigCallback(ctx) {
1161
1155
  user: cmdUser, user_id: cmdUserId, source: 'inline-button',
1162
1156
  }), `log ${setting} change`);
1163
1157
 
1164
- // Graceful respawn across all sessionKeys for this chat (matches the
1165
- // text-command flow in handleMessage).
1158
+ // Graceful respawn of the topic's session that the card is in. With
1159
+ // isolateTopics=false sessionKey is the chat (one shared session). With
1160
+ // isolateTopics=true sessionKey carries the topic, so other topics'
1161
+ // in-flight turns are not disturbed and the card update + button toast
1162
+ // only affect the user's own context. Mirrors the text-command flow in
1163
+ // handleMessage's requestRespawnForSession.
1164
+ const callbackThreadId = ctx.callbackQuery.message?.message_thread_id?.toString() || null;
1165
+ const callbackSessionKey = getSessionKey(chatId, callbackThreadId, chatConfig);
1166
1166
  const reason = setting === 'model' ? 'model-change' : 'effort-change';
1167
- const prefix = chatId;
1168
- let anyActive = false;
1169
- for (const key of pm.keys()) {
1170
- if (key === prefix || key.startsWith(prefix + ':')) {
1171
- const res = pm.requestRespawn(key, reason);
1172
- if (!res.killed) anyActive = true;
1173
- }
1174
- }
1167
+ const respawn = pm.requestRespawn(callbackSessionKey, reason);
1168
+ const anyActive = !respawn.killed;
1175
1169
 
1176
1170
  // Re-render the card with updated ✓ + the same help text shown initially.
1177
1171
  // Detect original card type (model-only / effort-only / both) by counting
@@ -1307,23 +1301,17 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
1307
1301
  await sendReply(info, { params: { reply_markup } });
1308
1302
  return;
1309
1303
  }
1310
- // Helper: request respawn across ALL sessionKeys owned by this chat (one
1311
- // per topic if isolateTopics=true, otherwise just the single chat-level
1312
- // key). Graceful: in-flight turns drain on old settings, new turns use
1313
- // the new settings. Returns total pending turns across all keys so the
1314
- // reply can tell the user.
1315
- const requestRespawnForChat = (reason) => {
1316
- const prefix = String(chatId);
1317
- let totalQueued = 0;
1318
- let anyActive = false;
1319
- for (const key of pm.keys()) {
1320
- if (key === prefix || key.startsWith(prefix + ':')) {
1321
- const res = pm.requestRespawn(key, reason);
1322
- totalQueued += res.queued;
1323
- if (!res.killed) anyActive = true;
1324
- }
1325
- }
1326
- return { queued: totalQueued, anyActive };
1304
+ // Graceful respawn of the user's CURRENT session only. With
1305
+ // isolateTopics=false the sessionKey is just the chat (one shared
1306
+ // session for the whole chat every topic respawns implicitly).
1307
+ // With isolateTopics=true each topic is a separate session, and a
1308
+ // /model in topic A should NOT disturb topic B's in-flight turn or
1309
+ // post a phantom "✓ Using sonnet now" in a topic that didn't ask.
1310
+ // Pre-0.6.5 this iterated pm.keys() by chat prefix and incorrectly
1311
+ // fanned out across all topics under isolateTopics=true.
1312
+ const requestRespawnForSession = (reason) => {
1313
+ const res = pm.requestRespawn(sessionKey, reason);
1314
+ return { queued: res.queued, anyActive: !res.killed };
1327
1315
  };
1328
1316
 
1329
1317
  if (botAllowsCommands && text.startsWith('/model ')) {
@@ -1337,7 +1325,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
1337
1325
  old_value: oldModel, new_value: newModel,
1338
1326
  user: cmdUser, user_id: cmdUserId, source: 'command',
1339
1327
  }), 'log model change');
1340
- const { anyActive } = requestRespawnForChat('model-change');
1328
+ const { anyActive } = requestRespawnForSession('model-change');
1341
1329
  const ver = MODEL_VERSIONS[newModel] || newModel;
1342
1330
  const suffix = anyActive ? ` — I'll switch when I finish` : '';
1343
1331
  await sendReply(`Model → ${newModel} (${ver})${suffix}`);
@@ -1357,7 +1345,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
1357
1345
  old_value: oldEffort, new_value: newEffort,
1358
1346
  user: cmdUser, user_id: cmdUserId, source: 'command',
1359
1347
  }), 'log effort change');
1360
- const { anyActive } = requestRespawnForChat('effort-change');
1348
+ const { anyActive } = requestRespawnForSession('effort-change');
1361
1349
  const suffix = anyActive ? ` — I'll switch when I finish` : '';
1362
1350
  await sendReply(`Effort → ${newEffort}${suffix}`);
1363
1351
  } else {
@@ -1870,16 +1858,24 @@ function createBot(token) {
1870
1858
  const threadId = msg.message_thread_id?.toString();
1871
1859
  const sessionKey = getSessionKey(chatId, threadId, chatConfig);
1872
1860
  const hadActive = pm.has(sessionKey) && !!pm.get(sessionKey)?.inFlight;
1873
- const dropped = drainQueuesForChat(chatId);
1874
1861
  // Mark BEFORE killing: the 'close' event fires almost immediately
1875
1862
  // after SIGTERM, and processQueue's catch needs to see the flag to
1876
1863
  // skip the generic error-reply. If we marked after, there'd be a
1877
1864
  // race where the error-reply slips through.
1878
1865
  if (hadActive) markSessionAborted(sessionKey);
1879
- await pm.killChat(chatId).catch(() => {});
1866
+ // Kill ONLY the user's own session, not every topic in the chat.
1867
+ // Pre-0.6.5 this was pm.killChat(chatId) which fanned out across
1868
+ // all topics under isolateTopics=true: the user typed "stop" in
1869
+ // topic A and the bot tore down topic B's in-flight turn, surfacing
1870
+ // a 💥 reply to topic B's user (whose key was never marked aborted,
1871
+ // so the abort grace window didn't apply). With isolateTopics=false
1872
+ // the sessionKey is the chat itself, so killing one session is the
1873
+ // same as killing the chat — behavior unchanged for the common case.
1874
+ await pm.kill(sessionKey).catch((err) =>
1875
+ console.error(`[${BOT_NAME}] abort kill failed: ${err.message}`));
1880
1876
  dbWrite(() => db.logEvent('abort-requested', {
1881
1877
  chat_id: chatId, user_id: msg.from?.id || null,
1882
- had_active: hadActive, queued_dropped: dropped,
1878
+ had_active: hadActive,
1883
1879
  trigger: cleanText.slice(0, 40),
1884
1880
  }), 'log abort-requested');
1885
1881
  // Reply in the same language the user aborted in. Cyrillic-detection
@@ -2088,8 +2084,10 @@ function createBot(token) {
2088
2084
  config.chats[newChatId] = { ...config.chats[oldChatId] };
2089
2085
  delete config.chats[oldChatId];
2090
2086
  saveConfig();
2091
- const droppedMigrate = drainQueuesForChat(oldChatId);
2092
- if (droppedMigrate) dbWrite(() => db.logEvent('queue-drained', { chat_id: oldChatId, reason: 'chat-migrated', dropped: droppedMigrate }), 'log queue-drained');
2087
+ // Chat migration is the one legit chat-wide kill: every session
2088
+ // (every topic) under the old chat_id is stale and must restart
2089
+ // under the new chat_id. Other respawn/abort paths target a
2090
+ // single sessionKey, but here ALL sessions are invalid.
2093
2091
  await pm.killChat(oldChatId);
2094
2092
  }
2095
2093
  });